@qpjoy/electron-launcher 0.2.0
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/README.md +164 -0
- package/dist/index.d.ts +79 -0
- package/dist/index.js +62 -0
- package/dist/network-diagnostics.d.ts +42 -0
- package/dist/network-diagnostics.js +254 -0
- package/dist/network-ownership-registry.d.ts +68 -0
- package/dist/network-ownership-registry.js +286 -0
- package/dist/release-updater.d.ts +129 -0
- package/dist/release-updater.js +195 -0
- package/dist/standalone-data-plane.d.ts +119 -0
- package/dist/standalone-data-plane.js +441 -0
- package/dist/system-domain-proxy.d.ts +91 -0
- package/dist/system-domain-proxy.js +2729 -0
- package/dist/wireguard.d.ts +289 -0
- package/dist/wireguard.js +902 -0
- package/package.json +61 -0
|
@@ -0,0 +1,2729 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { createSocket } from 'node:dgram';
|
|
3
|
+
import { createServer, request as httpRequest } from 'node:http';
|
|
4
|
+
import { request as httpsRequest } from 'node:https';
|
|
5
|
+
import { connect as netConnect, isIP } from 'node:net';
|
|
6
|
+
import { dirname, join } from 'node:path';
|
|
7
|
+
import { mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { buildElectronLauncherNetworkOwnershipRegistry, mergedElectronLauncherDnsZones, mergedElectronLauncherReverseProxyRoutes, resolveElectronLauncherDnsOwner } from './network-ownership-registry.js';
|
|
9
|
+
const STATE_VERSION = 1;
|
|
10
|
+
const DEFAULT_STATE_FILE = 'electron-launcher-system-domain-proxy.json';
|
|
11
|
+
const PAC_PATH = '/proxy.pac';
|
|
12
|
+
const SHARED_STATUS_PATH = '/__electron-launcher/domain-proxy/status';
|
|
13
|
+
const SHARED_APPLY_PATH = '/__electron-launcher/domain-proxy/apply';
|
|
14
|
+
const SHARED_RELEASE_PATH = '/__electron-launcher/domain-proxy/release';
|
|
15
|
+
const PAC_MARKER = 'MX_ELECTRON_LAUNCHER_PAC';
|
|
16
|
+
const DARWIN_RESOLVER_MARKER = 'MX_ELECTRON_LAUNCHER_RESOLVER';
|
|
17
|
+
const DARWIN_RESOLVER_DIR = '/etc/resolver';
|
|
18
|
+
const DARWIN_DYNAMIC_DNS_KEY = 'State:/Network/Service/com.qpjoy.electron-launcher.domain-proxy/DNS';
|
|
19
|
+
const WINDOWS_PROXY_KEY = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings';
|
|
20
|
+
const DNS_QUERY_TIMEOUT_MS = 1500;
|
|
21
|
+
const PROXY_CONNECT_TIMEOUT_MS = 10_000;
|
|
22
|
+
const INTERNAL_GATEWAY_APP_PORTS = new Set([80, 8008]);
|
|
23
|
+
export function createElectronLauncherSystemDomainProxy(options) {
|
|
24
|
+
const statePath = options.statePath || join(options.userDataDir, DEFAULT_STATE_FILE);
|
|
25
|
+
const log = options.log || console;
|
|
26
|
+
let localPacServer = null;
|
|
27
|
+
let localDnsServer = null;
|
|
28
|
+
let localPacPort = null;
|
|
29
|
+
let localPacKey = null;
|
|
30
|
+
let localPacConfig = null;
|
|
31
|
+
function localConfigFromPac(pac) {
|
|
32
|
+
return applyOwnershipRegistryToLocalConfig({
|
|
33
|
+
domains: pac.domains,
|
|
34
|
+
proxy: pac.proxy,
|
|
35
|
+
matchMode: pac.matchMode,
|
|
36
|
+
fallbackProxy: pac.fallbackProxy,
|
|
37
|
+
dnsServers: pac.dnsServers,
|
|
38
|
+
dnsFallbackTarget: pac.dnsFallbackTarget,
|
|
39
|
+
reverseProxyRoutes: pac.reverseProxyRoutes,
|
|
40
|
+
ownershipClaims: normalizeOwnershipClaims(pac.ownershipClaim ? [pac.ownershipClaim] : [])
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
async function closeLocalPacServer() {
|
|
44
|
+
const server = localPacServer;
|
|
45
|
+
const dnsServer = localDnsServer;
|
|
46
|
+
localPacServer = null;
|
|
47
|
+
localDnsServer = null;
|
|
48
|
+
localPacPort = null;
|
|
49
|
+
localPacKey = null;
|
|
50
|
+
localPacConfig = null;
|
|
51
|
+
if (dnsServer) {
|
|
52
|
+
await new Promise((resolve) => dnsServer.close(() => resolve()));
|
|
53
|
+
}
|
|
54
|
+
if (!server)
|
|
55
|
+
return;
|
|
56
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
57
|
+
}
|
|
58
|
+
async function ensureLocalPacServer(pac) {
|
|
59
|
+
const key = JSON.stringify({
|
|
60
|
+
domains: pac.domains,
|
|
61
|
+
matchMode: pac.matchMode,
|
|
62
|
+
proxy: pac.proxy?.directive || null,
|
|
63
|
+
fallbackProxy: pac.fallbackProxy?.directive || null,
|
|
64
|
+
dnsServers: pac.dnsServers,
|
|
65
|
+
dnsFallbackTarget: pac.dnsFallbackTarget,
|
|
66
|
+
reverseProxyRoutes: pac.reverseProxyRoutes,
|
|
67
|
+
ownershipClaim: pac.ownershipClaim?.ownerId || null,
|
|
68
|
+
directCidrs: pacDirectCidrs(pac.ownershipClaim ? [pac.ownershipClaim] : []),
|
|
69
|
+
pacPort: pac.pacPort || null
|
|
70
|
+
});
|
|
71
|
+
if (localPacServer && localPacPort && localPacKey === key) {
|
|
72
|
+
return {
|
|
73
|
+
pacUrl: `http://127.0.0.1:${localPacPort}${PAC_PATH}`,
|
|
74
|
+
port: localPacPort,
|
|
75
|
+
sharedLocalPac: false
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const nextConfig = localConfigFromPac(pac);
|
|
79
|
+
if (localPacServer && localPacPort) {
|
|
80
|
+
localPacConfig = nextConfig;
|
|
81
|
+
localPacKey = key;
|
|
82
|
+
return {
|
|
83
|
+
pacUrl: `http://127.0.0.1:${localPacPort}${PAC_PATH}`,
|
|
84
|
+
port: localPacPort,
|
|
85
|
+
sharedLocalPac: false
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const preferredPort = pac.pacPort || normalizePort(options.pacPort) || 0;
|
|
89
|
+
const server = createServer((req, res) => {
|
|
90
|
+
void handleLocalEdgeRequest(req, res);
|
|
91
|
+
});
|
|
92
|
+
server.on('connect', (req, socket, head) => {
|
|
93
|
+
void handleProxyConnectRequest(req, socket, head);
|
|
94
|
+
});
|
|
95
|
+
try {
|
|
96
|
+
await new Promise((resolve, reject) => {
|
|
97
|
+
server.once('error', reject);
|
|
98
|
+
server.listen(preferredPort, '127.0.0.1', () => {
|
|
99
|
+
server.off('error', reject);
|
|
100
|
+
const address = server.address();
|
|
101
|
+
if (!address || typeof address === 'string') {
|
|
102
|
+
reject(new Error('failed to allocate Electron Launcher PAC port'));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
localPacServer = server;
|
|
106
|
+
localPacPort = address.port;
|
|
107
|
+
localPacKey = key;
|
|
108
|
+
localPacConfig = nextConfig;
|
|
109
|
+
resolve();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
await ensureLocalDnsServer(localPacPort || preferredPort);
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
try {
|
|
116
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// The server may have failed before entering the listening state.
|
|
120
|
+
}
|
|
121
|
+
if (preferredPort > 0 && isAddressInUseError(err)) {
|
|
122
|
+
const shared = await registerSharedLocalPacServer(preferredPort, nextConfig);
|
|
123
|
+
const sharedConfig = normalizeSharedLocalPacConfig(shared.config || shared);
|
|
124
|
+
return {
|
|
125
|
+
pacUrl: shared.pacUrl,
|
|
126
|
+
port: preferredPort,
|
|
127
|
+
config: sharedConfig,
|
|
128
|
+
sharedLocalPac: true
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
throw err;
|
|
132
|
+
}
|
|
133
|
+
const activePort = localPacPort;
|
|
134
|
+
if (!activePort)
|
|
135
|
+
throw new Error('failed to allocate Electron Launcher PAC port');
|
|
136
|
+
return {
|
|
137
|
+
pacUrl: `http://127.0.0.1:${activePort}${PAC_PATH}`,
|
|
138
|
+
port: activePort,
|
|
139
|
+
sharedLocalPac: false
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
async function handleLocalEdgeRequest(req, res) {
|
|
143
|
+
const controlPath = localControlPath(req);
|
|
144
|
+
if (controlPath === PAC_PATH) {
|
|
145
|
+
const config = localPacConfig;
|
|
146
|
+
if (!config) {
|
|
147
|
+
writeTextResponse(res, 503, 'Electron Launcher PAC is not configured');
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const body = req.method === 'HEAD' ? '' : renderElectronLauncherPacScript(config);
|
|
151
|
+
res.writeHead(200, {
|
|
152
|
+
'Content-Type': 'application/x-ns-proxy-autoconfig; charset=utf-8',
|
|
153
|
+
'Cache-Control': 'no-store',
|
|
154
|
+
'Content-Length': String(Buffer.byteLength(body, 'utf8')),
|
|
155
|
+
Connection: 'close'
|
|
156
|
+
});
|
|
157
|
+
res.end(body);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (controlPath === SHARED_STATUS_PATH) {
|
|
161
|
+
writeJsonResponse(res, 200, {
|
|
162
|
+
marker: PAC_MARKER,
|
|
163
|
+
pacUrl: localPacPort ? `http://127.0.0.1:${localPacPort}${PAC_PATH}` : null,
|
|
164
|
+
domains: localPacConfig?.domains || [],
|
|
165
|
+
proxy: localPacConfig?.proxy?.address || null,
|
|
166
|
+
proxyDirective: localPacConfig?.proxy?.directive || null,
|
|
167
|
+
matchMode: localPacConfig?.matchMode || null,
|
|
168
|
+
fallbackProxy: localPacConfig?.fallbackProxy?.address || null,
|
|
169
|
+
fallbackProxyDirective: localPacConfig?.fallbackProxy?.directive || null,
|
|
170
|
+
dnsServers: localPacConfig?.dnsServers || [],
|
|
171
|
+
dnsFallbackTarget: localPacConfig?.dnsFallbackTarget || null,
|
|
172
|
+
reverseProxyRoutes: localPacConfig?.reverseProxyRoutes || [],
|
|
173
|
+
ownershipClaims: localPacConfig?.ownershipClaims || [],
|
|
174
|
+
ownershipRegistry: localPacConfig ? localConfigOwnershipRegistry(localPacConfig) : null
|
|
175
|
+
});
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (controlPath === SHARED_APPLY_PATH && req.method === 'POST') {
|
|
179
|
+
const body = await readRequestJson(req);
|
|
180
|
+
localPacConfig = mergeLocalPacConfigs(localPacConfig, normalizeSharedLocalPacConfig(body));
|
|
181
|
+
localPacKey = localPacConfigKey(localPacConfig, localPacPort);
|
|
182
|
+
writeJsonResponse(res, 200, {
|
|
183
|
+
marker: PAC_MARKER,
|
|
184
|
+
pacUrl: localPacPort ? `http://127.0.0.1:${localPacPort}${PAC_PATH}` : null,
|
|
185
|
+
shared: false,
|
|
186
|
+
domains: localPacConfig.domains,
|
|
187
|
+
proxy: localPacConfig.proxy?.address || null,
|
|
188
|
+
proxyDirective: localPacConfig.proxy?.directive || null,
|
|
189
|
+
matchMode: localPacConfig.matchMode,
|
|
190
|
+
fallbackProxy: localPacConfig.fallbackProxy?.address || null,
|
|
191
|
+
fallbackProxyDirective: localPacConfig.fallbackProxy?.directive || null,
|
|
192
|
+
dnsServers: localPacConfig.dnsServers,
|
|
193
|
+
dnsFallbackTarget: localPacConfig.dnsFallbackTarget,
|
|
194
|
+
reverseProxyRoutes: localPacConfig.reverseProxyRoutes,
|
|
195
|
+
ownershipClaims: localPacConfig.ownershipClaims,
|
|
196
|
+
ownershipRegistry: localConfigOwnershipRegistry(localPacConfig)
|
|
197
|
+
});
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (controlPath === SHARED_RELEASE_PATH && req.method === 'POST') {
|
|
201
|
+
const body = await readRequestJson(req);
|
|
202
|
+
const ownerId = normalizeOwnerId(body?.ownerId);
|
|
203
|
+
if (ownerId && localPacConfig) {
|
|
204
|
+
localPacConfig = releaseLocalPacConfigOwner(localPacConfig, ownerId);
|
|
205
|
+
localPacKey = localPacConfigKey(localPacConfig, localPacPort);
|
|
206
|
+
}
|
|
207
|
+
writeJsonResponse(res, 200, {
|
|
208
|
+
marker: PAC_MARKER,
|
|
209
|
+
pacUrl: localPacPort ? `http://127.0.0.1:${localPacPort}${PAC_PATH}` : null,
|
|
210
|
+
releasedOwnerId: ownerId,
|
|
211
|
+
domains: localPacConfig?.domains || [],
|
|
212
|
+
proxy: localPacConfig?.proxy?.address || null,
|
|
213
|
+
proxyDirective: localPacConfig?.proxy?.directive || null,
|
|
214
|
+
matchMode: localPacConfig?.matchMode || null,
|
|
215
|
+
fallbackProxy: localPacConfig?.fallbackProxy?.address || null,
|
|
216
|
+
fallbackProxyDirective: localPacConfig?.fallbackProxy?.directive || null,
|
|
217
|
+
dnsServers: localPacConfig?.dnsServers || [],
|
|
218
|
+
dnsFallbackTarget: localPacConfig?.dnsFallbackTarget || null,
|
|
219
|
+
reverseProxyRoutes: localPacConfig?.reverseProxyRoutes || [],
|
|
220
|
+
ownershipClaims: localPacConfig?.ownershipClaims || [],
|
|
221
|
+
ownershipRegistry: localPacConfig ? localConfigOwnershipRegistry(localPacConfig) : null
|
|
222
|
+
});
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
await handleProxyHttpRequest(req, res, localPacConfig);
|
|
226
|
+
}
|
|
227
|
+
async function ensureLocalDnsServer(port) {
|
|
228
|
+
if (!port || localDnsServer)
|
|
229
|
+
return;
|
|
230
|
+
const socket = createSocket('udp4');
|
|
231
|
+
socket.on('message', (message, remote) => {
|
|
232
|
+
void handleLocalDnsRelay(message, remote, socket);
|
|
233
|
+
});
|
|
234
|
+
socket.on('error', (err) => {
|
|
235
|
+
log.warn('[electron-launcher] local DNS relay error', err);
|
|
236
|
+
});
|
|
237
|
+
try {
|
|
238
|
+
await new Promise((resolve, reject) => {
|
|
239
|
+
socket.once('error', reject);
|
|
240
|
+
socket.bind(port, '127.0.0.1', () => {
|
|
241
|
+
socket.off('error', reject);
|
|
242
|
+
localDnsServer = socket;
|
|
243
|
+
resolve();
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
log.warn(`[electron-launcher] local DNS relay on 127.0.0.1:${port} unavailable`, err);
|
|
249
|
+
try {
|
|
250
|
+
socket.close();
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
// Ignore cleanup failures for a socket that never bound.
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
async function handleLocalDnsRelay(message, remote, socket) {
|
|
258
|
+
const config = localPacConfig;
|
|
259
|
+
const host = dnsQuestionHost(message);
|
|
260
|
+
if (!config || !host || !hostMatchesDomains(host, config.domains)) {
|
|
261
|
+
const failure = dnsFailureResponse(message);
|
|
262
|
+
if (failure.length)
|
|
263
|
+
socket.send(failure, remote.port, remote.address);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const routeTarget = dnsTargetForHost(host, config);
|
|
267
|
+
if (routeTarget && isIP(routeTarget) === 4) {
|
|
268
|
+
const directResponse = dnsAResponse(message, routeTarget);
|
|
269
|
+
if (directResponse.length) {
|
|
270
|
+
socket.send(directResponse, remote.port, remote.address);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const ownershipTarget = dnsOwnershipTargetForHost(host, config);
|
|
275
|
+
if (ownershipTarget && isIP(ownershipTarget) === 4) {
|
|
276
|
+
const directResponse = dnsAResponse(message, ownershipTarget);
|
|
277
|
+
if (directResponse.length) {
|
|
278
|
+
socket.send(directResponse, remote.port, remote.address);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (!config.dnsServers.length) {
|
|
283
|
+
const failure = dnsFailureResponse(message);
|
|
284
|
+
if (failure.length)
|
|
285
|
+
socket.send(failure, remote.port, remote.address);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
const response = await forwardDnsPacket(message, config.dnsServers);
|
|
290
|
+
if (shouldUseDnsFallbackResponse(message, response, config.dnsFallbackTarget)) {
|
|
291
|
+
const fallbackResponse = dnsAResponse(message, config.dnsFallbackTarget || '');
|
|
292
|
+
if (fallbackResponse.length) {
|
|
293
|
+
socket.send(fallbackResponse, remote.port, remote.address);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
socket.send(response, remote.port, remote.address);
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
if (config.dnsFallbackTarget && isIP(config.dnsFallbackTarget) === 4) {
|
|
301
|
+
const fallbackResponse = dnsAResponse(message, config.dnsFallbackTarget);
|
|
302
|
+
if (fallbackResponse.length) {
|
|
303
|
+
socket.send(fallbackResponse, remote.port, remote.address);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const failure = dnsFailureResponse(message);
|
|
308
|
+
if (failure.length)
|
|
309
|
+
socket.send(failure, remote.port, remote.address);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
async function handleProxyConnectRequest(req, clientSocket, head) {
|
|
313
|
+
const config = localPacConfig;
|
|
314
|
+
try {
|
|
315
|
+
const authority = parseProxyAuthority(req.url || '', 443);
|
|
316
|
+
if (!authority)
|
|
317
|
+
throw new Error(`unsupported CONNECT target: ${req.url || ''}`);
|
|
318
|
+
const routedTarget = connectRouteTarget(authority, config);
|
|
319
|
+
const targetHost = routedTarget?.host || await resolveProxyHost(authority.host, config);
|
|
320
|
+
const upstream = netConnect({
|
|
321
|
+
host: targetHost,
|
|
322
|
+
port: routedTarget?.port || authority.port,
|
|
323
|
+
timeout: PROXY_CONNECT_TIMEOUT_MS
|
|
324
|
+
});
|
|
325
|
+
upstream.once('connect', () => {
|
|
326
|
+
clientSocket.write('HTTP/1.1 200 Connection Established\r\nProxy-Agent: MX-H2I\r\n\r\n');
|
|
327
|
+
if (head.length)
|
|
328
|
+
upstream.write(head);
|
|
329
|
+
upstream.pipe(clientSocket);
|
|
330
|
+
clientSocket.pipe(upstream);
|
|
331
|
+
});
|
|
332
|
+
upstream.once('timeout', () => {
|
|
333
|
+
upstream.destroy(new Error(`proxy CONNECT timeout: ${authority.host}:${authority.port}`));
|
|
334
|
+
});
|
|
335
|
+
upstream.once('error', (err) => {
|
|
336
|
+
destroyProxyClient(clientSocket, `HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n${errorMessage(err)}`);
|
|
337
|
+
});
|
|
338
|
+
clientSocket.once('error', () => upstream.destroy());
|
|
339
|
+
}
|
|
340
|
+
catch (err) {
|
|
341
|
+
destroyProxyClient(clientSocket, `HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n${errorMessage(err)}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
async function resolvePacSource(policy, previous) {
|
|
345
|
+
if (policy?.enabled !== true)
|
|
346
|
+
return null;
|
|
347
|
+
const domains = normalizeDomains(policy.domains);
|
|
348
|
+
const pacUrl = stringValue(policy.pacUrl);
|
|
349
|
+
const pacPort = normalizePort(policy.pacPort) || normalizePort(options.pacPort);
|
|
350
|
+
const dnsServers = normalizeDnsServers(policy.dnsServers);
|
|
351
|
+
const dnsFallbackTarget = normalizeDnsTarget(policy.dnsFallbackTarget);
|
|
352
|
+
const systemResolverMode = normalizeSystemResolverMode(policy.systemResolver);
|
|
353
|
+
const reverseProxyRoutes = normalizeReverseProxyRoutes(policy.reverseProxyRoutes);
|
|
354
|
+
const fallbackProxy = normalizeProxyAddress(policy.fallbackProxy) || fallbackProxyForPac(previous);
|
|
355
|
+
const ownershipClaim = normalizeOwnershipClaim(policy.ownershipClaim);
|
|
356
|
+
if (pacUrl) {
|
|
357
|
+
return {
|
|
358
|
+
pacUrl,
|
|
359
|
+
domains,
|
|
360
|
+
proxy: normalizeProxyAddress(policy.proxy),
|
|
361
|
+
matchMode: normalizeMatchMode(policy.matchMode, policy.proxy),
|
|
362
|
+
fallbackProxy,
|
|
363
|
+
pacPort: null,
|
|
364
|
+
sharedLocalPac: false,
|
|
365
|
+
dnsServers,
|
|
366
|
+
dnsFallbackTarget,
|
|
367
|
+
systemResolverMode,
|
|
368
|
+
reverseProxyRoutes,
|
|
369
|
+
ownershipClaim,
|
|
370
|
+
ownershipClaims: normalizeOwnershipClaims(ownershipClaim ? [ownershipClaim] : []),
|
|
371
|
+
usesLocalPac: false
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
if (domains.length === 0)
|
|
375
|
+
return null;
|
|
376
|
+
const matchMode = normalizeMatchMode(policy.matchMode, policy.proxy);
|
|
377
|
+
const proxy = matchMode === 'proxy' ? normalizeProxyAddress(policy.proxy) : null;
|
|
378
|
+
if (matchMode === 'proxy' && !proxy)
|
|
379
|
+
return null;
|
|
380
|
+
const localPac = {
|
|
381
|
+
domains,
|
|
382
|
+
proxy,
|
|
383
|
+
matchMode,
|
|
384
|
+
fallbackProxy,
|
|
385
|
+
pacPort,
|
|
386
|
+
sharedLocalPac: false,
|
|
387
|
+
dnsServers,
|
|
388
|
+
dnsFallbackTarget,
|
|
389
|
+
systemResolverMode,
|
|
390
|
+
reverseProxyRoutes,
|
|
391
|
+
ownershipClaim,
|
|
392
|
+
ownershipClaims: normalizeOwnershipClaims(ownershipClaim ? [ownershipClaim] : [])
|
|
393
|
+
};
|
|
394
|
+
const localServer = await ensureLocalPacServer(localPac);
|
|
395
|
+
const effectiveConfig = localServer.config || localConfigFromPac(localPac);
|
|
396
|
+
return {
|
|
397
|
+
...localPac,
|
|
398
|
+
pacUrl: localServer.pacUrl,
|
|
399
|
+
pacPort: localServer.port,
|
|
400
|
+
domains: effectiveConfig.domains,
|
|
401
|
+
dnsServers: effectiveConfig.dnsServers,
|
|
402
|
+
dnsFallbackTarget: effectiveConfig.dnsFallbackTarget,
|
|
403
|
+
reverseProxyRoutes: effectiveConfig.reverseProxyRoutes,
|
|
404
|
+
ownershipClaims: effectiveConfig.ownershipClaims,
|
|
405
|
+
sharedLocalPac: localServer.sharedLocalPac,
|
|
406
|
+
usesLocalPac: true
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
async function prepareApplyState(policy) {
|
|
410
|
+
const existing = readState(statePath);
|
|
411
|
+
const previous = existing?.applied === true && existing.platform === process.platform
|
|
412
|
+
? existing.previous
|
|
413
|
+
: await capturePlatformState();
|
|
414
|
+
const pac = await resolvePacSource(policy, previous);
|
|
415
|
+
if (!pac)
|
|
416
|
+
return null;
|
|
417
|
+
const resolverPlan = systemResolverPlan(pac);
|
|
418
|
+
const next = {
|
|
419
|
+
version: STATE_VERSION,
|
|
420
|
+
applied: true,
|
|
421
|
+
platform: process.platform,
|
|
422
|
+
pacUrl: pac.pacUrl,
|
|
423
|
+
proxy: pac.proxy?.address || null,
|
|
424
|
+
matchMode: pac.matchMode,
|
|
425
|
+
fallbackProxy: pac.fallbackProxy?.directive || null,
|
|
426
|
+
pacPort: pac.pacPort,
|
|
427
|
+
sharedLocalPac: pac.sharedLocalPac,
|
|
428
|
+
dnsServers: pac.dnsServers,
|
|
429
|
+
dnsFallbackTarget: pac.dnsFallbackTarget,
|
|
430
|
+
systemResolver: pac.systemResolverMode !== 'off',
|
|
431
|
+
systemResolverMode: pac.systemResolverMode,
|
|
432
|
+
resolverDomains: resolverPlan.domains,
|
|
433
|
+
resolverPort: resolverPlan.port,
|
|
434
|
+
resolverApplied: false,
|
|
435
|
+
resolverError: null,
|
|
436
|
+
domains: pac.domains,
|
|
437
|
+
reverseProxyRoutes: pac.reverseProxyRoutes,
|
|
438
|
+
ownershipClaim: pac.ownershipClaim,
|
|
439
|
+
ownershipRegistry: pac.ownershipClaims.length
|
|
440
|
+
? buildElectronLauncherNetworkOwnershipRegistry(pac.ownershipClaims)
|
|
441
|
+
: null,
|
|
442
|
+
previous,
|
|
443
|
+
updatedAt: new Date().toISOString()
|
|
444
|
+
};
|
|
445
|
+
const changed = systemDomainProxyStateChanged(existing, next);
|
|
446
|
+
writeState(statePath, {
|
|
447
|
+
...next,
|
|
448
|
+
pending: true,
|
|
449
|
+
updatedAt: new Date().toISOString()
|
|
450
|
+
});
|
|
451
|
+
return {
|
|
452
|
+
existing,
|
|
453
|
+
previous,
|
|
454
|
+
pac,
|
|
455
|
+
resolverPlan,
|
|
456
|
+
next,
|
|
457
|
+
changed
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
async function releaseSharedOwnerForState(state) {
|
|
461
|
+
const ownerId = stateOwnershipOwnerId(state);
|
|
462
|
+
const port = normalizePort(state.pacPort);
|
|
463
|
+
if (state.sharedLocalPac !== true || !ownerId || !port)
|
|
464
|
+
return null;
|
|
465
|
+
const response = await releaseSharedLocalPacServer(port, ownerId);
|
|
466
|
+
return normalizeSharedLocalPacConfig(response);
|
|
467
|
+
}
|
|
468
|
+
async function releaseLocalOwnerAndRetainSharedEdge(state) {
|
|
469
|
+
const ownerId = stateOwnershipOwnerId(state);
|
|
470
|
+
if (!ownerId || !localPacServer || !localPacPort || !localPacConfig)
|
|
471
|
+
return null;
|
|
472
|
+
const nextConfig = releaseLocalPacConfigOwner(localPacConfig, ownerId);
|
|
473
|
+
if (!nextConfig.ownershipClaims.length)
|
|
474
|
+
return null;
|
|
475
|
+
localPacConfig = nextConfig;
|
|
476
|
+
localPacKey = localPacConfigKey(nextConfig, localPacPort);
|
|
477
|
+
const pac = resolvedPacSourceFromLocalConfig(`http://127.0.0.1:${localPacPort}${PAC_PATH}`, localPacPort, storedSystemResolverMode(state), nextConfig);
|
|
478
|
+
const resolver = await applyPlatformPacAndSystemResolvers(pac, state.previous, state, log);
|
|
479
|
+
const next = {
|
|
480
|
+
...state,
|
|
481
|
+
applied: true,
|
|
482
|
+
pacUrl: pac.pacUrl,
|
|
483
|
+
proxy: pac.proxy?.address || null,
|
|
484
|
+
matchMode: pac.matchMode,
|
|
485
|
+
fallbackProxy: pac.fallbackProxy?.directive || null,
|
|
486
|
+
pacPort: pac.pacPort,
|
|
487
|
+
sharedLocalPac: false,
|
|
488
|
+
dnsServers: pac.dnsServers,
|
|
489
|
+
dnsFallbackTarget: pac.dnsFallbackTarget,
|
|
490
|
+
systemResolver: resolver.mode !== 'off',
|
|
491
|
+
systemResolverMode: resolver.mode,
|
|
492
|
+
resolverDomains: resolver.domains,
|
|
493
|
+
resolverPort: resolver.port,
|
|
494
|
+
resolverApplied: resolver.applied,
|
|
495
|
+
resolverError: resolver.error || null,
|
|
496
|
+
domains: pac.domains,
|
|
497
|
+
reverseProxyRoutes: pac.reverseProxyRoutes,
|
|
498
|
+
ownershipClaim: null,
|
|
499
|
+
ownershipRegistry: localConfigOwnershipRegistry(nextConfig),
|
|
500
|
+
pending: false,
|
|
501
|
+
updatedAt: new Date().toISOString()
|
|
502
|
+
};
|
|
503
|
+
delete next.pending;
|
|
504
|
+
writeState(statePath, next);
|
|
505
|
+
return next;
|
|
506
|
+
}
|
|
507
|
+
return {
|
|
508
|
+
async apply(policy) {
|
|
509
|
+
if (!isSupportedPlatform())
|
|
510
|
+
return unsupportedStatus();
|
|
511
|
+
const prepared = await prepareApplyState(policy);
|
|
512
|
+
if (!prepared) {
|
|
513
|
+
await closeLocalPacServer();
|
|
514
|
+
return this.disable('domain-proxy-disabled');
|
|
515
|
+
}
|
|
516
|
+
const { pac, previous, existing, next, changed } = prepared;
|
|
517
|
+
const resolver = await applyPlatformPacAndSystemResolvers(pac, previous, existing, log);
|
|
518
|
+
next.systemResolver = resolver.mode !== 'off';
|
|
519
|
+
next.systemResolverMode = resolver.mode;
|
|
520
|
+
next.resolverDomains = resolver.domains;
|
|
521
|
+
next.resolverPort = resolver.port;
|
|
522
|
+
next.resolverApplied = resolver.applied;
|
|
523
|
+
next.resolverError = resolver.error || null;
|
|
524
|
+
if (pac.usesLocalPac !== true)
|
|
525
|
+
await closeLocalPacServer();
|
|
526
|
+
delete next.pending;
|
|
527
|
+
next.updatedAt = new Date().toISOString();
|
|
528
|
+
writeState(statePath, next);
|
|
529
|
+
return publicState(next, { changed });
|
|
530
|
+
},
|
|
531
|
+
async darwinPrepareApply(policy) {
|
|
532
|
+
if (!isSupportedPlatform())
|
|
533
|
+
return unsupportedStatus();
|
|
534
|
+
if (process.platform !== 'darwin')
|
|
535
|
+
return unsupportedStatus({ platform: process.platform });
|
|
536
|
+
const prepared = await prepareApplyState(policy);
|
|
537
|
+
if (!prepared) {
|
|
538
|
+
await closeLocalPacServer();
|
|
539
|
+
return this.disable('domain-proxy-disabled');
|
|
540
|
+
}
|
|
541
|
+
const shell = await darwinPlatformAndSystemApplyShell(prepared.pac.pacUrl, prepared.previous, prepared.existing, prepared.resolverPlan);
|
|
542
|
+
if (!shell) {
|
|
543
|
+
return publicState(prepared.next, {
|
|
544
|
+
changed: prepared.changed,
|
|
545
|
+
pending: true,
|
|
546
|
+
externalApply: false,
|
|
547
|
+
darwinApplyShell: null,
|
|
548
|
+
skipped: true,
|
|
549
|
+
reason: 'darwin-apply-shell-unavailable'
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
return publicState(prepared.next, {
|
|
553
|
+
changed: prepared.changed,
|
|
554
|
+
pending: true,
|
|
555
|
+
externalApply: true,
|
|
556
|
+
darwinApplyShell: shell
|
|
557
|
+
});
|
|
558
|
+
},
|
|
559
|
+
async completeExternalApply(reason = 'external') {
|
|
560
|
+
const existing = readState(statePath);
|
|
561
|
+
if (!existing?.applied || existing.platform !== process.platform) {
|
|
562
|
+
return {
|
|
563
|
+
supported: true,
|
|
564
|
+
applied: false,
|
|
565
|
+
platform: process.platform,
|
|
566
|
+
reason,
|
|
567
|
+
skipped: true
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
const resolver = await verifySystemResolvers(existing).catch((err) => ({
|
|
571
|
+
applied: false,
|
|
572
|
+
platform: process.platform,
|
|
573
|
+
mode: storedSystemResolverMode(existing),
|
|
574
|
+
domains: [],
|
|
575
|
+
error: errorMessage(err)
|
|
576
|
+
}));
|
|
577
|
+
const actual = await verifyPlatformPac(existing.pacUrl, existing.previous).catch((err) => ({
|
|
578
|
+
applied: false,
|
|
579
|
+
platform: process.platform,
|
|
580
|
+
error: errorMessage(err)
|
|
581
|
+
}));
|
|
582
|
+
const next = {
|
|
583
|
+
...existing,
|
|
584
|
+
systemResolver: resolver.mode !== 'off',
|
|
585
|
+
systemResolverMode: resolver.mode,
|
|
586
|
+
resolverDomains: normalizeDomains(existing.resolverDomains),
|
|
587
|
+
resolverPort: normalizePort(existing.resolverPort),
|
|
588
|
+
resolverApplied: resolver.applied,
|
|
589
|
+
resolverError: resolver.error || null,
|
|
590
|
+
pending: false,
|
|
591
|
+
updatedAt: new Date().toISOString()
|
|
592
|
+
};
|
|
593
|
+
delete next.pending;
|
|
594
|
+
writeState(statePath, next);
|
|
595
|
+
return publicState(next, {
|
|
596
|
+
reason,
|
|
597
|
+
verified: true,
|
|
598
|
+
externalApply: true,
|
|
599
|
+
actual: {
|
|
600
|
+
pac: actual,
|
|
601
|
+
resolver
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
},
|
|
605
|
+
async disable(reason = 'manual') {
|
|
606
|
+
if (!isSupportedPlatform())
|
|
607
|
+
return unsupportedStatus({ reason });
|
|
608
|
+
const existing = readState(statePath);
|
|
609
|
+
if (!existing?.applied || existing.platform !== process.platform) {
|
|
610
|
+
await closeLocalPacServer();
|
|
611
|
+
return {
|
|
612
|
+
supported: true,
|
|
613
|
+
applied: false,
|
|
614
|
+
platform: process.platform,
|
|
615
|
+
reason,
|
|
616
|
+
skipped: true
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
const retained = await releaseLocalOwnerAndRetainSharedEdge(existing).catch((err) => {
|
|
620
|
+
log.warn('[electron-launcher] failed to release local PAC owner before restore', err);
|
|
621
|
+
return null;
|
|
622
|
+
});
|
|
623
|
+
if (retained) {
|
|
624
|
+
return publicState(retained, {
|
|
625
|
+
reason,
|
|
626
|
+
changed: true,
|
|
627
|
+
skipped: true
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
await releaseSharedOwnerForState(existing).catch((err) => {
|
|
631
|
+
log.warn('[electron-launcher] failed to release shared PAC owner before restore', err);
|
|
632
|
+
});
|
|
633
|
+
await restorePlatformAndSystemState(existing, log);
|
|
634
|
+
await closeLocalPacServer();
|
|
635
|
+
removeState(statePath, log);
|
|
636
|
+
return {
|
|
637
|
+
supported: true,
|
|
638
|
+
applied: false,
|
|
639
|
+
platform: process.platform,
|
|
640
|
+
reason,
|
|
641
|
+
restored: true
|
|
642
|
+
};
|
|
643
|
+
},
|
|
644
|
+
async restoreStale(reason = 'startup') {
|
|
645
|
+
if (!isSupportedPlatform())
|
|
646
|
+
return unsupportedStatus({ reason });
|
|
647
|
+
const existing = readState(statePath);
|
|
648
|
+
if (existing?.applied === true && existing.platform === process.platform) {
|
|
649
|
+
await restorePlatformAndSystemState(existing, log);
|
|
650
|
+
await closeLocalPacServer();
|
|
651
|
+
removeState(statePath, log);
|
|
652
|
+
return {
|
|
653
|
+
supported: true,
|
|
654
|
+
applied: false,
|
|
655
|
+
platform: process.platform,
|
|
656
|
+
reason,
|
|
657
|
+
restored: true,
|
|
658
|
+
staleState: true
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
const orphanCleanup = await restoreOrphanSystemResolvers(log);
|
|
662
|
+
await closeLocalPacServer();
|
|
663
|
+
return {
|
|
664
|
+
supported: true,
|
|
665
|
+
applied: false,
|
|
666
|
+
platform: process.platform,
|
|
667
|
+
reason,
|
|
668
|
+
skipped: !orphanCleanup,
|
|
669
|
+
restored: orphanCleanup,
|
|
670
|
+
orphanCleanup
|
|
671
|
+
};
|
|
672
|
+
},
|
|
673
|
+
darwinRestoreScript() {
|
|
674
|
+
if (process.platform !== 'darwin')
|
|
675
|
+
return null;
|
|
676
|
+
const existing = readState(statePath);
|
|
677
|
+
if (existing?.applied === true && existing.platform === process.platform) {
|
|
678
|
+
if (requiresManagedRelease(existing, localPacConfig))
|
|
679
|
+
return null;
|
|
680
|
+
return darwinPlatformAndSystemRestoreShell(existing);
|
|
681
|
+
}
|
|
682
|
+
return darwinOrphanSystemResolverRestoreShell();
|
|
683
|
+
},
|
|
684
|
+
async completeExternalRestore(reason = 'external') {
|
|
685
|
+
const existing = readState(statePath);
|
|
686
|
+
if (existing?.applied === true && existing.platform === process.platform) {
|
|
687
|
+
await releaseSharedOwnerForState(existing).catch((err) => {
|
|
688
|
+
log.warn('[electron-launcher] failed to release shared PAC owner after external restore', err);
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
await closeLocalPacServer();
|
|
692
|
+
removeState(statePath, log);
|
|
693
|
+
return {
|
|
694
|
+
supported: true,
|
|
695
|
+
applied: false,
|
|
696
|
+
platform: process.platform,
|
|
697
|
+
reason,
|
|
698
|
+
restored: true
|
|
699
|
+
};
|
|
700
|
+
},
|
|
701
|
+
status() {
|
|
702
|
+
if (!isSupportedPlatform())
|
|
703
|
+
return unsupportedStatus();
|
|
704
|
+
const state = readState(statePath);
|
|
705
|
+
if (!state?.applied || state.platform !== process.platform) {
|
|
706
|
+
return {
|
|
707
|
+
supported: true,
|
|
708
|
+
applied: false,
|
|
709
|
+
platform: process.platform
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
return publicState(state);
|
|
713
|
+
},
|
|
714
|
+
async statusVerified() {
|
|
715
|
+
if (!isSupportedPlatform())
|
|
716
|
+
return unsupportedStatus();
|
|
717
|
+
const state = readState(statePath);
|
|
718
|
+
if (!state?.applied || state.platform !== process.platform) {
|
|
719
|
+
return {
|
|
720
|
+
supported: true,
|
|
721
|
+
applied: false,
|
|
722
|
+
verified: true,
|
|
723
|
+
platform: process.platform
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
try {
|
|
727
|
+
const pacVerification = await verifyPlatformPac(state.pacUrl, state.previous);
|
|
728
|
+
const resolverVerification = await verifySystemResolvers(state);
|
|
729
|
+
return publicState(state, {
|
|
730
|
+
applied: pacVerification.applied && resolverVerification.applied,
|
|
731
|
+
verified: true,
|
|
732
|
+
actual: {
|
|
733
|
+
pac: pacVerification,
|
|
734
|
+
resolver: resolverVerification
|
|
735
|
+
},
|
|
736
|
+
resolverApplied: resolverVerification.applied,
|
|
737
|
+
resolverError: resolverVerification.error || null
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
catch (err) {
|
|
741
|
+
return publicState(state, {
|
|
742
|
+
applied: false,
|
|
743
|
+
verified: false,
|
|
744
|
+
error: errorMessage(err)
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
},
|
|
748
|
+
close: closeLocalPacServer
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
export function renderElectronLauncherPacScript(input) {
|
|
752
|
+
const matchDirective = input.matchMode === 'proxy' && input.proxy
|
|
753
|
+
? input.proxy.directive
|
|
754
|
+
: 'DIRECT';
|
|
755
|
+
const fallbackDirective = input.fallbackProxy
|
|
756
|
+
? `${input.fallbackProxy.directive}; DIRECT`
|
|
757
|
+
: 'DIRECT';
|
|
758
|
+
const directCidrs = pacDirectCidrs(input.ownershipClaims);
|
|
759
|
+
return `// ${PAC_MARKER}
|
|
760
|
+
function FindProxyForURL(url, host) {
|
|
761
|
+
var h = String(host || '').toLowerCase();
|
|
762
|
+
var directCidrs = ${JSON.stringify(directCidrs)};
|
|
763
|
+
if (isIpv4Literal(h)) {
|
|
764
|
+
for (var j = 0; j < directCidrs.length; j++) {
|
|
765
|
+
var c = directCidrs[j];
|
|
766
|
+
if (isInNet(h, c.base, c.mask)) {
|
|
767
|
+
return 'DIRECT';
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
var domains = ${JSON.stringify(normalizeDomains(input.domains))};
|
|
772
|
+
for (var i = 0; i < domains.length; i++) {
|
|
773
|
+
var d = domains[i];
|
|
774
|
+
if (h === d || h.slice(-(d.length + 1)) === '.' + d) {
|
|
775
|
+
return ${JSON.stringify(matchDirective)};
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
return ${JSON.stringify(fallbackDirective)};
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function isIpv4Literal(value) {
|
|
782
|
+
if (!/^\\d{1,3}(?:\\.\\d{1,3}){3}$/.test(value)) return false;
|
|
783
|
+
var parts = value.split('.');
|
|
784
|
+
for (var i = 0; i < parts.length; i++) {
|
|
785
|
+
var n = Number(parts[i]);
|
|
786
|
+
if (!isFinite(n) || n < 0 || n > 255 || Math.floor(n) !== n) return false;
|
|
787
|
+
}
|
|
788
|
+
return true;
|
|
789
|
+
}
|
|
790
|
+
`;
|
|
791
|
+
}
|
|
792
|
+
function isSupportedPlatform() {
|
|
793
|
+
return process.platform === 'darwin' || process.platform === 'win32';
|
|
794
|
+
}
|
|
795
|
+
function unsupportedStatus(extra = {}) {
|
|
796
|
+
return {
|
|
797
|
+
supported: false,
|
|
798
|
+
applied: false,
|
|
799
|
+
platform: process.platform,
|
|
800
|
+
...extra
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
async function capturePlatformState() {
|
|
804
|
+
if (process.platform === 'darwin')
|
|
805
|
+
return captureDarwinState();
|
|
806
|
+
if (process.platform === 'win32')
|
|
807
|
+
return captureWindowsState();
|
|
808
|
+
return {};
|
|
809
|
+
}
|
|
810
|
+
async function applyPlatformPac(pacUrl, previous) {
|
|
811
|
+
if (process.platform === 'darwin') {
|
|
812
|
+
await applyDarwinPac(pacUrl, previous);
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
if (process.platform === 'win32') {
|
|
816
|
+
await applyWindowsPac(pacUrl);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
async function applyPlatformPacAndSystemResolvers(pac, previous, existing, log) {
|
|
820
|
+
const plan = systemResolverPlan(pac);
|
|
821
|
+
if (process.platform === 'darwin' && plan.mode === 'dynamic' && plan.port && plan.domains.length > 0) {
|
|
822
|
+
return applyDarwinPacAndDynamicResolvers(pac.pacUrl, previous, existing, { ...plan, port: plan.port }, log);
|
|
823
|
+
}
|
|
824
|
+
await applyPlatformPac(pac.pacUrl, previous);
|
|
825
|
+
return applySystemResolversWithPlan(pac, existing, plan, log);
|
|
826
|
+
}
|
|
827
|
+
async function restorePlatformState(previous) {
|
|
828
|
+
if (process.platform === 'darwin') {
|
|
829
|
+
await restoreDarwinState(previous);
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
if (process.platform === 'win32') {
|
|
833
|
+
await restoreWindowsState(previous);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
async function restorePlatformAndSystemState(state, log) {
|
|
837
|
+
if (process.platform === 'darwin') {
|
|
838
|
+
await restoreDarwinPlatformAndSystemState(state, log);
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
await restorePlatformState(state.previous);
|
|
842
|
+
await restoreSystemResolvers(state, log);
|
|
843
|
+
}
|
|
844
|
+
async function restoreOrphanSystemResolvers(log) {
|
|
845
|
+
if (process.platform !== 'darwin')
|
|
846
|
+
return false;
|
|
847
|
+
try {
|
|
848
|
+
await runDarwinPrivilegedShell(darwinOrphanSystemResolverRestoreShell());
|
|
849
|
+
return true;
|
|
850
|
+
}
|
|
851
|
+
catch (err) {
|
|
852
|
+
log.warn('[electron-launcher] failed to remove orphan macOS split DNS resolver', err);
|
|
853
|
+
return false;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
async function verifyPlatformPac(pacUrl, previous) {
|
|
857
|
+
if (process.platform === 'darwin')
|
|
858
|
+
return verifyDarwinPac(pacUrl, previous);
|
|
859
|
+
if (process.platform === 'win32')
|
|
860
|
+
return verifyWindowsPac(pacUrl);
|
|
861
|
+
return { applied: false, platform: process.platform };
|
|
862
|
+
}
|
|
863
|
+
async function applySystemResolvers(pac, existing, log) {
|
|
864
|
+
const plan = systemResolverPlan(pac);
|
|
865
|
+
return applySystemResolversWithPlan(pac, existing, plan, log);
|
|
866
|
+
}
|
|
867
|
+
async function applySystemResolversWithPlan(_pac, existing, plan, log) {
|
|
868
|
+
if (systemResolverPlanMatches(existing, plan)) {
|
|
869
|
+
const current = await verifySystemResolvers(existing);
|
|
870
|
+
if (current.applied) {
|
|
871
|
+
return {
|
|
872
|
+
mode: plan.mode,
|
|
873
|
+
domains: plan.domains,
|
|
874
|
+
port: plan.port,
|
|
875
|
+
applied: true,
|
|
876
|
+
error: null
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
await removeStaleSystemResolvers(existing, plan, log);
|
|
881
|
+
if (process.platform !== 'darwin' || plan.mode === 'off' || !plan.port || plan.domains.length === 0) {
|
|
882
|
+
return {
|
|
883
|
+
mode: plan.mode,
|
|
884
|
+
domains: plan.domains,
|
|
885
|
+
port: plan.port,
|
|
886
|
+
applied: true,
|
|
887
|
+
error: null
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
try {
|
|
891
|
+
if (plan.mode === 'dynamic')
|
|
892
|
+
await applyDarwinDynamicResolvers(plan.domains, plan.port);
|
|
893
|
+
else
|
|
894
|
+
await applyDarwinResolvers(plan.domains, plan.port);
|
|
895
|
+
const verification = plan.mode === 'dynamic'
|
|
896
|
+
? await verifyDarwinDynamicResolvers(plan.domains, plan.port)
|
|
897
|
+
: verifyDarwinFileResolvers(plan.domains, plan.port);
|
|
898
|
+
return {
|
|
899
|
+
mode: plan.mode,
|
|
900
|
+
domains: plan.domains,
|
|
901
|
+
port: plan.port,
|
|
902
|
+
applied: verification.applied,
|
|
903
|
+
error: verification.error || null
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
catch (err) {
|
|
907
|
+
const message = darwinAuthorizationErrorMessage(err);
|
|
908
|
+
log.warn('[electron-launcher] failed to apply macOS split DNS resolver', err);
|
|
909
|
+
return {
|
|
910
|
+
mode: plan.mode,
|
|
911
|
+
domains: plan.domains,
|
|
912
|
+
port: plan.port,
|
|
913
|
+
applied: false,
|
|
914
|
+
error: message
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
function systemResolverPlanMatches(existing, plan) {
|
|
919
|
+
if (!existing?.applied || existing.platform !== process.platform)
|
|
920
|
+
return false;
|
|
921
|
+
if (storedSystemResolverMode(existing) !== plan.mode)
|
|
922
|
+
return false;
|
|
923
|
+
if ((normalizePort(existing.resolverPort) || null) !== (plan.port || null))
|
|
924
|
+
return false;
|
|
925
|
+
const previousDomains = normalizeDomains(existing.resolverDomains).sort();
|
|
926
|
+
const nextDomains = normalizeDomains(plan.domains).sort();
|
|
927
|
+
return previousDomains.length === nextDomains.length
|
|
928
|
+
&& previousDomains.every((domain, index) => domain === nextDomains[index]);
|
|
929
|
+
}
|
|
930
|
+
async function verifySystemResolvers(state) {
|
|
931
|
+
const mode = storedSystemResolverMode(state);
|
|
932
|
+
if (process.platform !== 'darwin') {
|
|
933
|
+
return {
|
|
934
|
+
applied: true,
|
|
935
|
+
platform: process.platform,
|
|
936
|
+
mode,
|
|
937
|
+
domains: []
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
const domains = normalizeDomains(state.resolverDomains);
|
|
941
|
+
const port = normalizePort(state.resolverPort);
|
|
942
|
+
if (mode === 'off' || !domains.length || !port) {
|
|
943
|
+
return {
|
|
944
|
+
applied: true,
|
|
945
|
+
platform: process.platform,
|
|
946
|
+
mode,
|
|
947
|
+
domains: []
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
return mode === 'dynamic'
|
|
951
|
+
? verifyDarwinDynamicResolvers(domains, port)
|
|
952
|
+
: verifyDarwinFileResolvers(domains, port);
|
|
953
|
+
}
|
|
954
|
+
async function restoreSystemResolvers(state, log) {
|
|
955
|
+
if (process.platform !== 'darwin')
|
|
956
|
+
return;
|
|
957
|
+
try {
|
|
958
|
+
await removeDarwinDynamicResolvers();
|
|
959
|
+
await removeDarwinResolvers(normalizeDomains(state.resolverDomains));
|
|
960
|
+
}
|
|
961
|
+
catch (err) {
|
|
962
|
+
log.warn('[electron-launcher] failed to remove macOS split DNS resolver', err);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
async function removeStaleSystemResolvers(existing, next, log) {
|
|
966
|
+
if (process.platform !== 'darwin' || !existing)
|
|
967
|
+
return;
|
|
968
|
+
const previousMode = storedSystemResolverMode(existing);
|
|
969
|
+
const previousDomains = normalizeDomains(existing.resolverDomains);
|
|
970
|
+
try {
|
|
971
|
+
if (previousMode === 'dynamic' && (next.mode !== 'dynamic' || existing.resolverPort !== next.port)) {
|
|
972
|
+
await removeDarwinDynamicResolvers();
|
|
973
|
+
}
|
|
974
|
+
const nextFileDomains = next.mode === 'file' ? new Set(normalizeDomains(next.domains)) : new Set();
|
|
975
|
+
const staleFiles = previousDomains.filter((domain) => !nextFileDomains.has(domain));
|
|
976
|
+
if (staleFiles.length)
|
|
977
|
+
await removeDarwinResolvers(staleFiles);
|
|
978
|
+
}
|
|
979
|
+
catch (err) {
|
|
980
|
+
log.warn('[electron-launcher] failed to remove stale macOS split DNS resolver', err);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
function systemResolverPlan(pac) {
|
|
984
|
+
if (process.platform !== 'darwin' || pac.usesLocalPac !== true || pac.systemResolverMode === 'off') {
|
|
985
|
+
return {
|
|
986
|
+
mode: 'off',
|
|
987
|
+
domains: [],
|
|
988
|
+
port: null
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
return {
|
|
992
|
+
mode: pac.systemResolverMode,
|
|
993
|
+
domains: darwinResolverDomains(pac.domains),
|
|
994
|
+
port: normalizePort(pac.pacPort)
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
function darwinResolverDomains(domains) {
|
|
998
|
+
const roots = [];
|
|
999
|
+
const candidates = normalizeDomains(domains)
|
|
1000
|
+
.filter(isDarwinResolverDomain)
|
|
1001
|
+
.sort((left, right) => left.length - right.length || left.localeCompare(right));
|
|
1002
|
+
for (const domain of candidates) {
|
|
1003
|
+
if (roots.some((root) => domain === root || domain.endsWith(`.${root}`)))
|
|
1004
|
+
continue;
|
|
1005
|
+
roots.push(domain);
|
|
1006
|
+
}
|
|
1007
|
+
return roots;
|
|
1008
|
+
}
|
|
1009
|
+
async function applyDarwinDynamicResolvers(domains, port) {
|
|
1010
|
+
const script = darwinDynamicResolverScript(domains, port);
|
|
1011
|
+
if (!script)
|
|
1012
|
+
return;
|
|
1013
|
+
await runScutilScript(script);
|
|
1014
|
+
}
|
|
1015
|
+
function darwinDynamicResolverScript(domains, port) {
|
|
1016
|
+
const normalized = normalizeDomains(domains).filter(isDarwinResolverDomain);
|
|
1017
|
+
if (normalized.length === 0)
|
|
1018
|
+
return null;
|
|
1019
|
+
return [
|
|
1020
|
+
'd.init',
|
|
1021
|
+
'd.add ServerAddresses * 127.0.0.1',
|
|
1022
|
+
`d.add ServerPort # ${port}`,
|
|
1023
|
+
`d.add SupplementalMatchDomains * ${normalized.map(scutilToken).join(' ')}`,
|
|
1024
|
+
`d.add SupplementalMatchOrders * ${normalized.map((_, index) => String(50 + index)).join(' ')}`,
|
|
1025
|
+
'd.add SupplementalMatchDomainsNoSearch # 1',
|
|
1026
|
+
`set ${DARWIN_DYNAMIC_DNS_KEY}`,
|
|
1027
|
+
'quit',
|
|
1028
|
+
''
|
|
1029
|
+
].join('\n');
|
|
1030
|
+
}
|
|
1031
|
+
async function applyDarwinPacAndDynamicResolvers(pacUrl, previous, existing, plan, log) {
|
|
1032
|
+
if (existing?.applied && existing.pacUrl === pacUrl && systemResolverPlanMatches(existing, plan)) {
|
|
1033
|
+
const pacVerification = await verifyDarwinPac(pacUrl, previous).catch(() => null);
|
|
1034
|
+
const resolverVerification = await verifyDarwinDynamicResolvers(plan.domains, plan.port).catch(() => null);
|
|
1035
|
+
if (pacVerification?.applied === true && resolverVerification?.applied === true) {
|
|
1036
|
+
return {
|
|
1037
|
+
mode: plan.mode,
|
|
1038
|
+
domains: plan.domains,
|
|
1039
|
+
port: plan.port,
|
|
1040
|
+
applied: true,
|
|
1041
|
+
error: null
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
const shell = await darwinPlatformAndSystemApplyShell(pacUrl, previous, existing, plan);
|
|
1046
|
+
if (!shell) {
|
|
1047
|
+
await applyDarwinPac(pacUrl, previous);
|
|
1048
|
+
return {
|
|
1049
|
+
mode: plan.mode,
|
|
1050
|
+
domains: plan.domains,
|
|
1051
|
+
port: plan.port,
|
|
1052
|
+
applied: true,
|
|
1053
|
+
error: null
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
try {
|
|
1057
|
+
await runDarwinPrivilegedShell(shell);
|
|
1058
|
+
const verification = await verifyDarwinDynamicResolvers(plan.domains, plan.port);
|
|
1059
|
+
return {
|
|
1060
|
+
mode: plan.mode,
|
|
1061
|
+
domains: plan.domains,
|
|
1062
|
+
port: plan.port,
|
|
1063
|
+
applied: verification.applied,
|
|
1064
|
+
error: verification.error || null
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
catch (err) {
|
|
1068
|
+
const message = darwinAuthorizationErrorMessage(err);
|
|
1069
|
+
log.warn('[electron-launcher] failed to apply macOS PAC and dynamic split DNS resolver', err);
|
|
1070
|
+
return {
|
|
1071
|
+
mode: plan.mode,
|
|
1072
|
+
domains: plan.domains,
|
|
1073
|
+
port: plan.port,
|
|
1074
|
+
applied: false,
|
|
1075
|
+
error: message
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
async function darwinPlatformAndSystemApplyShell(pacUrl, previous, existing, plan) {
|
|
1080
|
+
if (process.platform !== 'darwin' || plan.mode !== 'dynamic' || !plan.port)
|
|
1081
|
+
return null;
|
|
1082
|
+
const resolverScript = darwinDynamicResolverScript(plan.domains, plan.port);
|
|
1083
|
+
if (!resolverScript)
|
|
1084
|
+
return null;
|
|
1085
|
+
let services = darwinServiceNames(previous);
|
|
1086
|
+
if (services.length === 0)
|
|
1087
|
+
services = await listDarwinNetworkServices();
|
|
1088
|
+
if (services.length === 0)
|
|
1089
|
+
throw new Error('macOS network services not found');
|
|
1090
|
+
return [
|
|
1091
|
+
'set -e',
|
|
1092
|
+
...darwinStaleResolverFileRemovalCommands(existing, plan),
|
|
1093
|
+
...services.flatMap((name) => [
|
|
1094
|
+
['/usr/sbin/networksetup', '-setautoproxyurl', name, pacUrl].map(shellQuote).join(' '),
|
|
1095
|
+
['/usr/sbin/networksetup', '-setautoproxystate', name, 'on'].map(shellQuote).join(' ')
|
|
1096
|
+
]),
|
|
1097
|
+
`/usr/bin/printf %s ${shellQuote(resolverScript)} | /usr/sbin/scutil`
|
|
1098
|
+
].join('\n');
|
|
1099
|
+
}
|
|
1100
|
+
async function restoreDarwinPlatformAndSystemState(state, log) {
|
|
1101
|
+
try {
|
|
1102
|
+
await runDarwinPrivilegedShell(darwinPlatformAndSystemRestoreShell(state));
|
|
1103
|
+
}
|
|
1104
|
+
catch (err) {
|
|
1105
|
+
log.warn('[electron-launcher] failed to restore macOS PAC and split DNS in one transaction', err);
|
|
1106
|
+
await restoreDarwinState(state.previous).catch((restoreErr) => {
|
|
1107
|
+
log.warn('[electron-launcher] failed to restore macOS PAC after combined restore failure', restoreErr);
|
|
1108
|
+
});
|
|
1109
|
+
await restoreSystemResolvers(state, log);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
function darwinPlatformAndSystemRestoreShell(state) {
|
|
1113
|
+
return [
|
|
1114
|
+
'set -e',
|
|
1115
|
+
...darwinAutoProxyRestoreCommands(state.previous),
|
|
1116
|
+
darwinDynamicResolverRemovalCommand(),
|
|
1117
|
+
...darwinOwnedResolverFileRemovalCommands(normalizeDomains(state.resolverDomains))
|
|
1118
|
+
].join('\n');
|
|
1119
|
+
}
|
|
1120
|
+
function darwinOrphanSystemResolverRestoreShell() {
|
|
1121
|
+
return [
|
|
1122
|
+
'set -e',
|
|
1123
|
+
darwinDynamicResolverRemovalCommand(),
|
|
1124
|
+
...darwinOwnedResolverFileRemovalCommands()
|
|
1125
|
+
].join('\n');
|
|
1126
|
+
}
|
|
1127
|
+
function darwinAutoProxyRestoreCommands(previous) {
|
|
1128
|
+
return previousServices(previous)
|
|
1129
|
+
.filter((service) => service.name)
|
|
1130
|
+
.flatMap((service) => {
|
|
1131
|
+
const name = String(service.name);
|
|
1132
|
+
const commands = [];
|
|
1133
|
+
if (service.url) {
|
|
1134
|
+
commands.push(['/usr/sbin/networksetup', '-setautoproxyurl', name, String(service.url)].map(shellQuote).join(' ') + ' || true');
|
|
1135
|
+
}
|
|
1136
|
+
commands.push(['/usr/sbin/networksetup', '-setautoproxystate', name, service.enabled === true ? 'on' : 'off'].map(shellQuote).join(' ') + ' || true');
|
|
1137
|
+
return commands;
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
function darwinDynamicResolverRemovalCommand() {
|
|
1141
|
+
const script = `remove ${DARWIN_DYNAMIC_DNS_KEY}\nquit\n`;
|
|
1142
|
+
return `/usr/bin/printf %s ${shellQuote(script)} | /usr/sbin/scutil >/dev/null 2>&1 || true`;
|
|
1143
|
+
}
|
|
1144
|
+
function darwinStaleResolverFileRemovalCommands(existing, next) {
|
|
1145
|
+
if (!existing)
|
|
1146
|
+
return [];
|
|
1147
|
+
const previousDomains = normalizeDomains(existing.resolverDomains).filter(isDarwinResolverDomain);
|
|
1148
|
+
const nextFileDomains = next.mode === 'file' ? new Set(normalizeDomains(next.domains)) : new Set();
|
|
1149
|
+
return previousDomains
|
|
1150
|
+
.filter((domain) => !nextFileDomains.has(domain))
|
|
1151
|
+
.map((domain) => darwinResolverPath(domain))
|
|
1152
|
+
.filter((filePath) => Boolean(filePath))
|
|
1153
|
+
.map((filePath) => `if [ -f ${shellQuote(filePath)} ] && /usr/bin/grep -q ${shellQuote(DARWIN_RESOLVER_MARKER)} ${shellQuote(filePath)}; then /bin/rm -f ${shellQuote(filePath)}; fi`);
|
|
1154
|
+
}
|
|
1155
|
+
function darwinOwnedResolverFileRemovalCommands(domains) {
|
|
1156
|
+
const files = domains && domains.length > 0
|
|
1157
|
+
? domains
|
|
1158
|
+
.map((domain) => darwinResolverPath(domain))
|
|
1159
|
+
.filter((filePath) => Boolean(filePath))
|
|
1160
|
+
: darwinOwnedResolverFiles();
|
|
1161
|
+
return uniqueList(files)
|
|
1162
|
+
.map((filePath) => `if [ -f ${shellQuote(filePath)} ] && /usr/bin/grep -q ${shellQuote(DARWIN_RESOLVER_MARKER)} ${shellQuote(filePath)}; then /bin/rm -f ${shellQuote(filePath)}; fi`);
|
|
1163
|
+
}
|
|
1164
|
+
function darwinOwnedResolverFiles() {
|
|
1165
|
+
try {
|
|
1166
|
+
return readdirSync(DARWIN_RESOLVER_DIR)
|
|
1167
|
+
.map((name) => join(DARWIN_RESOLVER_DIR, name))
|
|
1168
|
+
.filter((filePath) => hasOwnedDarwinResolverFile(filePath));
|
|
1169
|
+
}
|
|
1170
|
+
catch {
|
|
1171
|
+
return [];
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
async function verifyDarwinDynamicResolvers(domains, port) {
|
|
1175
|
+
const normalized = normalizeDomains(domains).filter(isDarwinResolverDomain);
|
|
1176
|
+
const result = await runScutilReadScript(`show ${DARWIN_DYNAMIC_DNS_KEY}\nquit\n`).catch((err) => ({
|
|
1177
|
+
stdout: '',
|
|
1178
|
+
stderr: errorMessage(err)
|
|
1179
|
+
}));
|
|
1180
|
+
const dns = await execFileText('/usr/sbin/scutil', ['--dns']).catch((err) => ({
|
|
1181
|
+
stdout: '',
|
|
1182
|
+
stderr: errorMessage(err)
|
|
1183
|
+
}));
|
|
1184
|
+
const text = `${result.stdout}\n${result.stderr}\n${dns.stdout}\n${dns.stderr}`;
|
|
1185
|
+
const rows = normalized.map((domain) => ({
|
|
1186
|
+
domain,
|
|
1187
|
+
applied: text.includes(domain)
|
|
1188
|
+
}));
|
|
1189
|
+
const hasServer = /127\.0\.0\.1/.test(text);
|
|
1190
|
+
const hasPort = new RegExp(`\\b${port}\\b`).test(text);
|
|
1191
|
+
const applied = rows.every((row) => row.applied) && hasServer && hasPort;
|
|
1192
|
+
return {
|
|
1193
|
+
applied,
|
|
1194
|
+
platform: process.platform,
|
|
1195
|
+
mode: 'dynamic',
|
|
1196
|
+
domains: rows,
|
|
1197
|
+
error: applied ? null : 'macOS dynamic resolver not applied'
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
async function removeDarwinDynamicResolvers() {
|
|
1201
|
+
await runScutilScript(`remove ${DARWIN_DYNAMIC_DNS_KEY}\nquit\n`).catch(() => undefined);
|
|
1202
|
+
}
|
|
1203
|
+
async function applyDarwinResolvers(domains, port) {
|
|
1204
|
+
try {
|
|
1205
|
+
writeDarwinResolverFiles(domains, port);
|
|
1206
|
+
}
|
|
1207
|
+
catch {
|
|
1208
|
+
await runDarwinResolverApplyScript(domains, port);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
function writeDarwinResolverFiles(domains, port) {
|
|
1212
|
+
mkdirSync(DARWIN_RESOLVER_DIR, { recursive: true });
|
|
1213
|
+
for (const domain of domains) {
|
|
1214
|
+
const filePath = darwinResolverPath(domain);
|
|
1215
|
+
if (!filePath)
|
|
1216
|
+
continue;
|
|
1217
|
+
if (hasForeignDarwinResolverFile(filePath))
|
|
1218
|
+
continue;
|
|
1219
|
+
writeFileSync(filePath, darwinResolverBody(domain, port));
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
async function runDarwinResolverApplyScript(domains, port) {
|
|
1223
|
+
if (domains.length === 0)
|
|
1224
|
+
return;
|
|
1225
|
+
const commands = ['/bin/mkdir -p /etc/resolver'];
|
|
1226
|
+
for (const domain of domains) {
|
|
1227
|
+
const filePath = darwinResolverPath(domain);
|
|
1228
|
+
if (!filePath)
|
|
1229
|
+
continue;
|
|
1230
|
+
commands.push([
|
|
1231
|
+
`if [ -e ${shellQuote(filePath)} ] && ! /usr/bin/grep -q ${shellQuote(DARWIN_RESOLVER_MARKER)} ${shellQuote(filePath)}; then`,
|
|
1232
|
+
' :',
|
|
1233
|
+
'else',
|
|
1234
|
+
` /usr/bin/printf %s ${shellQuote(darwinResolverBody(domain, port))} > ${shellQuote(filePath)}`,
|
|
1235
|
+
'fi'
|
|
1236
|
+
].join('\n'));
|
|
1237
|
+
}
|
|
1238
|
+
await runDarwinPrivilegedShell(commands.join('\n'));
|
|
1239
|
+
}
|
|
1240
|
+
async function removeDarwinResolvers(domains) {
|
|
1241
|
+
const normalized = normalizeDomains(domains).filter(isDarwinResolverDomain);
|
|
1242
|
+
if (normalized.length === 0)
|
|
1243
|
+
return;
|
|
1244
|
+
try {
|
|
1245
|
+
for (const domain of normalized) {
|
|
1246
|
+
const filePath = darwinResolverPath(domain);
|
|
1247
|
+
if (!filePath || !hasOwnedDarwinResolverFile(filePath))
|
|
1248
|
+
continue;
|
|
1249
|
+
rmSync(filePath, { force: true });
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
catch {
|
|
1253
|
+
const commands = normalized
|
|
1254
|
+
.map((domain) => darwinResolverPath(domain))
|
|
1255
|
+
.filter((filePath) => Boolean(filePath))
|
|
1256
|
+
.map((filePath) => `if [ -f ${shellQuote(filePath)} ] && /usr/bin/grep -q ${shellQuote(DARWIN_RESOLVER_MARKER)} ${shellQuote(filePath)}; then /bin/rm -f ${shellQuote(filePath)}; fi`);
|
|
1257
|
+
if (commands.length)
|
|
1258
|
+
await runDarwinPrivilegedShell(commands.join('\n'));
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
function verifyDarwinFileResolvers(domains, port) {
|
|
1262
|
+
const rows = normalizeDomains(domains).map((domain) => {
|
|
1263
|
+
const filePath = darwinResolverPath(domain);
|
|
1264
|
+
if (!filePath) {
|
|
1265
|
+
return {
|
|
1266
|
+
domain,
|
|
1267
|
+
applied: false,
|
|
1268
|
+
error: 'invalid resolver domain'
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
try {
|
|
1272
|
+
const text = readFileSync(filePath, 'utf8');
|
|
1273
|
+
const owned = text.includes(DARWIN_RESOLVER_MARKER);
|
|
1274
|
+
const hasNameserver = /^\s*nameserver\s+127\.0\.0\.1\s*$/im.test(text);
|
|
1275
|
+
const portMatch = text.match(/^\s*port\s+(\d+)\s*$/im);
|
|
1276
|
+
const portApplied = Number(portMatch?.[1]) === port;
|
|
1277
|
+
return {
|
|
1278
|
+
domain,
|
|
1279
|
+
filePath,
|
|
1280
|
+
owned,
|
|
1281
|
+
port,
|
|
1282
|
+
applied: owned && hasNameserver && portApplied
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
catch (err) {
|
|
1286
|
+
return {
|
|
1287
|
+
domain,
|
|
1288
|
+
filePath,
|
|
1289
|
+
applied: false,
|
|
1290
|
+
error: errorMessage(err)
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1293
|
+
});
|
|
1294
|
+
const failed = rows.find((row) => row.applied !== true);
|
|
1295
|
+
return {
|
|
1296
|
+
applied: rows.every((row) => row.applied === true),
|
|
1297
|
+
platform: process.platform,
|
|
1298
|
+
mode: 'file',
|
|
1299
|
+
domains: rows,
|
|
1300
|
+
error: failed ? `macOS resolver not applied for ${failed.domain || 'domain'}` : null
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
function darwinResolverBody(domain, port) {
|
|
1304
|
+
return [
|
|
1305
|
+
`# ${DARWIN_RESOLVER_MARKER}`,
|
|
1306
|
+
'# generated by @qpjoy/electron-launcher',
|
|
1307
|
+
`# domain ${domain}`,
|
|
1308
|
+
'nameserver 127.0.0.1',
|
|
1309
|
+
`port ${port}`,
|
|
1310
|
+
'timeout 1',
|
|
1311
|
+
'search_order 1',
|
|
1312
|
+
''
|
|
1313
|
+
].join('\n');
|
|
1314
|
+
}
|
|
1315
|
+
function darwinResolverPath(domain) {
|
|
1316
|
+
const clean = normalizeDomains([domain])[0];
|
|
1317
|
+
if (!clean || !isDarwinResolverDomain(clean))
|
|
1318
|
+
return null;
|
|
1319
|
+
return join(DARWIN_RESOLVER_DIR, clean);
|
|
1320
|
+
}
|
|
1321
|
+
function isDarwinResolverDomain(domain) {
|
|
1322
|
+
return /^[a-z0-9-]+(\.[a-z0-9-]+)*$/.test(domain);
|
|
1323
|
+
}
|
|
1324
|
+
function hasForeignDarwinResolverFile(filePath) {
|
|
1325
|
+
try {
|
|
1326
|
+
const text = readFileSync(filePath, 'utf8');
|
|
1327
|
+
return !text.includes(DARWIN_RESOLVER_MARKER);
|
|
1328
|
+
}
|
|
1329
|
+
catch {
|
|
1330
|
+
return false;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
function hasOwnedDarwinResolverFile(filePath) {
|
|
1334
|
+
try {
|
|
1335
|
+
return readFileSync(filePath, 'utf8').includes(DARWIN_RESOLVER_MARKER);
|
|
1336
|
+
}
|
|
1337
|
+
catch {
|
|
1338
|
+
return false;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
async function runDarwinPrivilegedShell(command) {
|
|
1342
|
+
await execFileText('/usr/bin/osascript', [
|
|
1343
|
+
'-e',
|
|
1344
|
+
`do shell script ${JSON.stringify(command)} with administrator privileges`
|
|
1345
|
+
]);
|
|
1346
|
+
}
|
|
1347
|
+
async function runScutilScript(script) {
|
|
1348
|
+
const command = `/usr/bin/printf %s ${shellQuote(script)} | /usr/sbin/scutil`;
|
|
1349
|
+
try {
|
|
1350
|
+
const result = await execFileText('/bin/sh', ['-c', command]);
|
|
1351
|
+
if (scutilNeedsPrivilege(result)) {
|
|
1352
|
+
throw new Error(scutilOutput(result) || 'scutil permission denied');
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
catch {
|
|
1356
|
+
await runDarwinPrivilegedShell(command);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
function runScutilReadScript(script) {
|
|
1360
|
+
const command = `/usr/bin/printf %s ${shellQuote(script)} | /usr/sbin/scutil`;
|
|
1361
|
+
return execFileText('/bin/sh', ['-c', command]);
|
|
1362
|
+
}
|
|
1363
|
+
function scutilToken(value) {
|
|
1364
|
+
return value.replace(/[^a-zA-Z0-9._-]/g, '');
|
|
1365
|
+
}
|
|
1366
|
+
function scutilNeedsPrivilege(result) {
|
|
1367
|
+
return /permission denied/i.test(scutilOutput(result));
|
|
1368
|
+
}
|
|
1369
|
+
function scutilOutput(result) {
|
|
1370
|
+
return `${result.stdout || ''}\n${result.stderr || ''}`.trim();
|
|
1371
|
+
}
|
|
1372
|
+
async function captureDarwinState() {
|
|
1373
|
+
const services = await listDarwinNetworkServices();
|
|
1374
|
+
const out = [];
|
|
1375
|
+
for (const name of services) {
|
|
1376
|
+
try {
|
|
1377
|
+
const autoProxy = await execFileText('/usr/sbin/networksetup', ['-getautoproxyurl', name]);
|
|
1378
|
+
const webProxy = await execFileText('/usr/sbin/networksetup', ['-getwebproxy', name]).catch(() => null);
|
|
1379
|
+
const secureWebProxy = await execFileText('/usr/sbin/networksetup', ['-getsecurewebproxy', name]).catch(() => null);
|
|
1380
|
+
const socksProxy = await execFileText('/usr/sbin/networksetup', ['-getsocksfirewallproxy', name]).catch(() => null);
|
|
1381
|
+
out.push({
|
|
1382
|
+
name,
|
|
1383
|
+
...parseDarwinAutoProxy(autoProxy.stdout),
|
|
1384
|
+
webProxy: webProxy ? parseDarwinProxy(webProxy.stdout, 'http') : null,
|
|
1385
|
+
secureWebProxy: secureWebProxy ? parseDarwinProxy(secureWebProxy.stdout, 'http') : null,
|
|
1386
|
+
socksProxy: socksProxy ? parseDarwinProxy(socksProxy.stdout, 'socks') : null
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
catch {
|
|
1390
|
+
// Ignore transient services that disappear while applying network settings.
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
return { services: out };
|
|
1394
|
+
}
|
|
1395
|
+
async function applyDarwinPac(pacUrl, previous) {
|
|
1396
|
+
let services = darwinServiceNames(previous);
|
|
1397
|
+
if (services.length === 0)
|
|
1398
|
+
services = await listDarwinNetworkServices();
|
|
1399
|
+
if (services.length === 0)
|
|
1400
|
+
throw new Error('macOS network services not found');
|
|
1401
|
+
const commands = [];
|
|
1402
|
+
for (const name of services) {
|
|
1403
|
+
commands.push(['-setautoproxyurl', name, pacUrl]);
|
|
1404
|
+
commands.push(['-setautoproxystate', name, 'on']);
|
|
1405
|
+
}
|
|
1406
|
+
await runDarwinNetworksetupSetBatch(commands);
|
|
1407
|
+
}
|
|
1408
|
+
async function verifyDarwinPac(pacUrl, previous) {
|
|
1409
|
+
let services = darwinServiceNames(previous);
|
|
1410
|
+
if (services.length === 0)
|
|
1411
|
+
services = await listDarwinNetworkServices();
|
|
1412
|
+
const rows = [];
|
|
1413
|
+
for (const name of services) {
|
|
1414
|
+
try {
|
|
1415
|
+
const result = await execFileText('/usr/sbin/networksetup', ['-getautoproxyurl', name]);
|
|
1416
|
+
const parsed = parseDarwinAutoProxy(result.stdout);
|
|
1417
|
+
rows.push({
|
|
1418
|
+
name,
|
|
1419
|
+
url: parsed.url,
|
|
1420
|
+
enabled: parsed.enabled,
|
|
1421
|
+
applied: parsed.enabled === true && parsed.url === pacUrl
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
catch (err) {
|
|
1425
|
+
rows.push({
|
|
1426
|
+
name,
|
|
1427
|
+
applied: false,
|
|
1428
|
+
error: errorMessage(err)
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
return {
|
|
1433
|
+
applied: rows.length > 0 && rows.every((row) => row.applied === true),
|
|
1434
|
+
platform: process.platform,
|
|
1435
|
+
pacUrl,
|
|
1436
|
+
services: rows
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
async function restoreDarwinState(previous) {
|
|
1440
|
+
const services = previousServices(previous);
|
|
1441
|
+
const commands = [];
|
|
1442
|
+
for (const service of services) {
|
|
1443
|
+
if (!service.name)
|
|
1444
|
+
continue;
|
|
1445
|
+
if (service.url) {
|
|
1446
|
+
commands.push(['-setautoproxyurl', service.name, service.url]);
|
|
1447
|
+
}
|
|
1448
|
+
commands.push([
|
|
1449
|
+
'-setautoproxystate',
|
|
1450
|
+
service.name,
|
|
1451
|
+
service.enabled === true ? 'on' : 'off'
|
|
1452
|
+
]);
|
|
1453
|
+
}
|
|
1454
|
+
await runDarwinNetworksetupSetBatch(commands);
|
|
1455
|
+
}
|
|
1456
|
+
async function listDarwinNetworkServices() {
|
|
1457
|
+
const result = await execFileText('/usr/sbin/networksetup', ['-listallnetworkservices']);
|
|
1458
|
+
return result.stdout
|
|
1459
|
+
.split(/\r?\n/)
|
|
1460
|
+
.map((line) => line.trim())
|
|
1461
|
+
.filter((line) => line && !line.startsWith('An asterisk') && !line.startsWith('*'));
|
|
1462
|
+
}
|
|
1463
|
+
function parseDarwinAutoProxy(stdout) {
|
|
1464
|
+
const urlMatch = stdout.match(/^URL:\s*(.*)$/im);
|
|
1465
|
+
const enabledMatch = stdout.match(/^Enabled:\s*(.*)$/im);
|
|
1466
|
+
const url = stringValue(urlMatch?.[1]);
|
|
1467
|
+
return {
|
|
1468
|
+
url: url && url !== '(null)' ? url : null,
|
|
1469
|
+
enabled: /^yes$/i.test(String(enabledMatch?.[1] || '').trim())
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
function parseDarwinProxy(stdout, kind) {
|
|
1473
|
+
const enabled = /^Enabled:\s*Yes$/im.test(stdout);
|
|
1474
|
+
const server = stringValue(stdout.match(/^Server:\s*(.*)$/im)?.[1]);
|
|
1475
|
+
const port = stringValue(stdout.match(/^Port:\s*(.*)$/im)?.[1]);
|
|
1476
|
+
if (!enabled || !server || !port)
|
|
1477
|
+
return null;
|
|
1478
|
+
return normalizeProxyAddress(`${kind === 'socks' ? 'socks://' : ''}${server}:${port}`);
|
|
1479
|
+
}
|
|
1480
|
+
function darwinServiceNames(previous) {
|
|
1481
|
+
return previousServices(previous).map((service) => service.name).filter(Boolean);
|
|
1482
|
+
}
|
|
1483
|
+
function previousServices(previous) {
|
|
1484
|
+
if (!previous || typeof previous !== 'object')
|
|
1485
|
+
return [];
|
|
1486
|
+
const services = previous.services;
|
|
1487
|
+
return Array.isArray(services)
|
|
1488
|
+
? services.filter((service) => Boolean(service && typeof service === 'object'))
|
|
1489
|
+
: [];
|
|
1490
|
+
}
|
|
1491
|
+
async function runDarwinNetworksetupSetBatch(commands) {
|
|
1492
|
+
if (commands.length === 0)
|
|
1493
|
+
return;
|
|
1494
|
+
try {
|
|
1495
|
+
for (const args of commands) {
|
|
1496
|
+
await execFileText('/usr/sbin/networksetup', args);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
catch {
|
|
1500
|
+
const command = [
|
|
1501
|
+
'set -e',
|
|
1502
|
+
...commands.map((args) => ['/usr/sbin/networksetup', ...args].map(shellQuote).join(' '))
|
|
1503
|
+
].join('\n');
|
|
1504
|
+
await execFileText('/usr/bin/osascript', [
|
|
1505
|
+
'-e',
|
|
1506
|
+
`do shell script ${JSON.stringify(command)} with administrator privileges`
|
|
1507
|
+
]);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
async function captureWindowsState() {
|
|
1511
|
+
return {
|
|
1512
|
+
autoConfigUrl: await queryWindowsRegistryValue('AutoConfigURL'),
|
|
1513
|
+
proxyEnable: await queryWindowsRegistryValue('ProxyEnable'),
|
|
1514
|
+
proxyServer: await queryWindowsRegistryValue('ProxyServer'),
|
|
1515
|
+
proxyOverride: await queryWindowsRegistryValue('ProxyOverride'),
|
|
1516
|
+
autoDetect: await queryWindowsRegistryValue('AutoDetect')
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
async function applyWindowsPac(pacUrl) {
|
|
1520
|
+
await addWindowsRegistryValue('AutoConfigURL', 'REG_SZ', pacUrl);
|
|
1521
|
+
await addWindowsRegistryValue('ProxyEnable', 'REG_DWORD', '0');
|
|
1522
|
+
await notifyWindowsProxyChanged();
|
|
1523
|
+
}
|
|
1524
|
+
async function verifyWindowsPac(pacUrl) {
|
|
1525
|
+
const autoConfigUrl = await queryWindowsRegistryValue('AutoConfigURL');
|
|
1526
|
+
return {
|
|
1527
|
+
applied: Boolean(autoConfigUrl.exists && autoConfigUrl.value === pacUrl),
|
|
1528
|
+
platform: process.platform,
|
|
1529
|
+
pacUrl,
|
|
1530
|
+
autoConfigUrl
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
async function restoreWindowsState(previous) {
|
|
1534
|
+
const row = previous && typeof previous === 'object' ? previous : {};
|
|
1535
|
+
await restoreWindowsRegistryValue('AutoConfigURL', row.autoConfigUrl, 'REG_SZ');
|
|
1536
|
+
await restoreWindowsRegistryValue('ProxyServer', row.proxyServer, 'REG_SZ');
|
|
1537
|
+
await restoreWindowsRegistryValue('ProxyOverride', row.proxyOverride, 'REG_SZ');
|
|
1538
|
+
await restoreWindowsRegistryValue('AutoDetect', row.autoDetect, 'REG_DWORD');
|
|
1539
|
+
await restoreWindowsRegistryValue('ProxyEnable', row.proxyEnable, 'REG_DWORD');
|
|
1540
|
+
await notifyWindowsProxyChanged();
|
|
1541
|
+
}
|
|
1542
|
+
async function restoreWindowsRegistryValue(name, value, fallbackType) {
|
|
1543
|
+
if (value?.exists) {
|
|
1544
|
+
await addWindowsRegistryValue(name, value.type || fallbackType, value.value || '');
|
|
1545
|
+
}
|
|
1546
|
+
else {
|
|
1547
|
+
await deleteWindowsRegistryValue(name);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
async function queryWindowsRegistryValue(name) {
|
|
1551
|
+
try {
|
|
1552
|
+
const result = await execFileText('reg.exe', ['query', WINDOWS_PROXY_KEY, '/v', name]);
|
|
1553
|
+
return parseWindowsRegistryValue(result.stdout, name) || { exists: false, name };
|
|
1554
|
+
}
|
|
1555
|
+
catch {
|
|
1556
|
+
return { exists: false, name };
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
function parseWindowsRegistryValue(stdout, name) {
|
|
1560
|
+
const lines = stdout.split(/\r?\n/);
|
|
1561
|
+
for (const line of lines) {
|
|
1562
|
+
const match = line.trim().match(/^(\S+)\s+(REG_\S+)\s*(.*)$/);
|
|
1563
|
+
if (!match || match[1] !== name)
|
|
1564
|
+
continue;
|
|
1565
|
+
return {
|
|
1566
|
+
exists: true,
|
|
1567
|
+
name,
|
|
1568
|
+
type: match[2],
|
|
1569
|
+
value: match[3] || ''
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
return null;
|
|
1573
|
+
}
|
|
1574
|
+
function addWindowsRegistryValue(name, type, value) {
|
|
1575
|
+
return execFileText('reg.exe', ['add', WINDOWS_PROXY_KEY, '/v', name, '/t', type, '/d', String(value), '/f']);
|
|
1576
|
+
}
|
|
1577
|
+
function deleteWindowsRegistryValue(name) {
|
|
1578
|
+
return execFileText('reg.exe', ['delete', WINDOWS_PROXY_KEY, '/v', name, '/f']).catch(() => undefined);
|
|
1579
|
+
}
|
|
1580
|
+
function notifyWindowsProxyChanged() {
|
|
1581
|
+
const script = [
|
|
1582
|
+
'$sig = @\'',
|
|
1583
|
+
'[DllImport("wininet.dll", SetLastError=true)] public static extern bool InternetSetOption(IntPtr hInternet, int dwOption, IntPtr lpBuffer, int dwBufferLength);',
|
|
1584
|
+
'\'@;',
|
|
1585
|
+
'$type = Add-Type -MemberDefinition $sig -Name WinInetNotify -Namespace QPJoy -PassThru;',
|
|
1586
|
+
'[void]$type::InternetSetOption([IntPtr]::Zero, 39, [IntPtr]::Zero, 0);',
|
|
1587
|
+
'[void]$type::InternetSetOption([IntPtr]::Zero, 37, [IntPtr]::Zero, 0);'
|
|
1588
|
+
].join(' ');
|
|
1589
|
+
return execFileText('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', script])
|
|
1590
|
+
.catch(() => undefined);
|
|
1591
|
+
}
|
|
1592
|
+
function fallbackProxyForPac(previous) {
|
|
1593
|
+
if (process.platform === 'darwin') {
|
|
1594
|
+
for (const service of previousServices(previous)) {
|
|
1595
|
+
const proxy = service.secureWebProxy || service.webProxy || service.socksProxy;
|
|
1596
|
+
if (proxy?.directive)
|
|
1597
|
+
return proxy;
|
|
1598
|
+
}
|
|
1599
|
+
return null;
|
|
1600
|
+
}
|
|
1601
|
+
if (process.platform !== 'win32' || !previous || typeof previous !== 'object')
|
|
1602
|
+
return null;
|
|
1603
|
+
const row = previous;
|
|
1604
|
+
const enabled = row.proxyEnable;
|
|
1605
|
+
const server = row.proxyServer;
|
|
1606
|
+
if (!enabled?.exists || String(enabled.value || '').trim() === '0')
|
|
1607
|
+
return null;
|
|
1608
|
+
if (!server?.exists || !server.value)
|
|
1609
|
+
return null;
|
|
1610
|
+
return normalizeWindowsProxyServer(server.value);
|
|
1611
|
+
}
|
|
1612
|
+
function normalizeWindowsProxyServer(value) {
|
|
1613
|
+
const text = stringValue(value);
|
|
1614
|
+
if (!text)
|
|
1615
|
+
return null;
|
|
1616
|
+
if (!text.includes('='))
|
|
1617
|
+
return normalizeProxyAddress(text);
|
|
1618
|
+
const entries = {};
|
|
1619
|
+
for (const part of text.split(';')) {
|
|
1620
|
+
const [rawKey, ...rawValue] = part.split('=');
|
|
1621
|
+
const key = String(rawKey || '').trim().toLowerCase();
|
|
1622
|
+
const candidate = rawValue.join('=').trim();
|
|
1623
|
+
if (!key || !candidate)
|
|
1624
|
+
continue;
|
|
1625
|
+
entries[key] = candidate;
|
|
1626
|
+
}
|
|
1627
|
+
return normalizeProxyAddress(entries.https)
|
|
1628
|
+
|| normalizeProxyAddress(entries.http)
|
|
1629
|
+
|| normalizeProxyAddress(entries.socks ? `socks://${entries.socks}` : null)
|
|
1630
|
+
|| normalizeProxyAddress(Object.values(entries).find(Boolean));
|
|
1631
|
+
}
|
|
1632
|
+
async function handleProxyHttpRequest(req, res, config) {
|
|
1633
|
+
if (!config) {
|
|
1634
|
+
writeTextResponse(res, 503, 'Electron Launcher proxy is not configured');
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
if (req.method === 'CONNECT') {
|
|
1638
|
+
writeTextResponse(res, 405, 'CONNECT must use the proxy tunnel path');
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
const target = proxyHttpTarget(req);
|
|
1642
|
+
if (!target) {
|
|
1643
|
+
writeTextResponse(res, 400, 'unsupported proxy request target');
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1646
|
+
if (target.protocol !== 'http:') {
|
|
1647
|
+
writeTextResponse(res, 501, `unsupported proxy protocol: ${target.protocol}`);
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
try {
|
|
1651
|
+
const route = reverseProxyRouteForHost(target.hostname, config);
|
|
1652
|
+
const routeUrl = route?.targetUrl ? new URL(route.targetUrl) : null;
|
|
1653
|
+
const targetHost = routeUrl
|
|
1654
|
+
? stripHostBrackets(routeUrl.hostname)
|
|
1655
|
+
: await resolveProxyHost(target.hostname, config);
|
|
1656
|
+
const targetPort = routeUrl
|
|
1657
|
+
? upstreamPort(routeUrl)
|
|
1658
|
+
: Number(target.port || 80);
|
|
1659
|
+
const requestImpl = routeUrl?.protocol === 'https:' ? httpsRequest : httpRequest;
|
|
1660
|
+
const upstreamHostHeader = routeUrl && route && shouldPreserveHostForGatewayRoute(route, routeUrl)
|
|
1661
|
+
? target.host
|
|
1662
|
+
: routeUrl?.host || target.host;
|
|
1663
|
+
const upstream = requestImpl({
|
|
1664
|
+
host: targetHost,
|
|
1665
|
+
port: targetPort,
|
|
1666
|
+
method: req.method,
|
|
1667
|
+
path: routeUrl ? reverseProxyPath(routeUrl, target) : `${target.pathname}${target.search}`,
|
|
1668
|
+
headers: proxyForwardHeaders(req, upstreamHostHeader, target.host),
|
|
1669
|
+
timeout: PROXY_CONNECT_TIMEOUT_MS
|
|
1670
|
+
}, (upstreamRes) => {
|
|
1671
|
+
res.writeHead(upstreamRes.statusCode || 502, upstreamRes.statusMessage || undefined, upstreamRes.headers);
|
|
1672
|
+
upstreamRes.pipe(res);
|
|
1673
|
+
});
|
|
1674
|
+
upstream.on('timeout', () => upstream.destroy(new Error(`proxy request timeout: ${target.host}`)));
|
|
1675
|
+
upstream.on('error', (err) => {
|
|
1676
|
+
if (!res.headersSent)
|
|
1677
|
+
writeTextResponse(res, 502, errorMessage(err));
|
|
1678
|
+
else
|
|
1679
|
+
res.destroy(err);
|
|
1680
|
+
});
|
|
1681
|
+
req.pipe(upstream);
|
|
1682
|
+
}
|
|
1683
|
+
catch (err) {
|
|
1684
|
+
writeTextResponse(res, 502, errorMessage(err));
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
async function resolveProxyHost(host, config) {
|
|
1688
|
+
const cleanHost = stripHostBrackets(host).toLowerCase();
|
|
1689
|
+
if (!cleanHost || isIP(cleanHost))
|
|
1690
|
+
return cleanHost;
|
|
1691
|
+
const routeTarget = dnsTargetForHost(cleanHost, config);
|
|
1692
|
+
if (routeTarget)
|
|
1693
|
+
return routeTarget;
|
|
1694
|
+
const ownershipTarget = dnsOwnershipTargetForHost(cleanHost, config);
|
|
1695
|
+
if (ownershipTarget)
|
|
1696
|
+
return ownershipTarget;
|
|
1697
|
+
if (!config?.dnsServers.length || !hostMatchesDomains(cleanHost, config.domains))
|
|
1698
|
+
return cleanHost;
|
|
1699
|
+
const failures = [];
|
|
1700
|
+
for (const server of config.dnsServers) {
|
|
1701
|
+
try {
|
|
1702
|
+
return await resolveDnsA(cleanHost, server);
|
|
1703
|
+
}
|
|
1704
|
+
catch (err) {
|
|
1705
|
+
failures.push(`${server}: ${errorMessage(err)}`);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
if (config.dnsFallbackTarget)
|
|
1709
|
+
return config.dnsFallbackTarget;
|
|
1710
|
+
throw new Error(`Internal DNS failed for ${cleanHost}: ${failures.join('; ')}`);
|
|
1711
|
+
}
|
|
1712
|
+
function reverseProxyRouteForHost(host, config) {
|
|
1713
|
+
const cleanHost = normalizeDomainName(stripHostBrackets(host));
|
|
1714
|
+
if (!cleanHost || !config?.reverseProxyRoutes.length)
|
|
1715
|
+
return null;
|
|
1716
|
+
return config.reverseProxyRoutes.find((route) => route.host === cleanHost) || null;
|
|
1717
|
+
}
|
|
1718
|
+
function dnsTargetForHost(host, config) {
|
|
1719
|
+
const route = reverseProxyRouteForHost(host, config);
|
|
1720
|
+
return route?.dnsTarget && isIP(route.dnsTarget) ? route.dnsTarget : null;
|
|
1721
|
+
}
|
|
1722
|
+
function dnsOwnershipTargetForHost(host, config) {
|
|
1723
|
+
if (!config?.ownershipClaims.length)
|
|
1724
|
+
return null;
|
|
1725
|
+
const registry = localConfigOwnershipRegistry(config);
|
|
1726
|
+
const owner = resolveElectronLauncherDnsOwner(registry, host);
|
|
1727
|
+
return owner?.target && isIP(owner.target) === 4 ? owner.target : null;
|
|
1728
|
+
}
|
|
1729
|
+
function connectRouteTarget(authority, config) {
|
|
1730
|
+
const route = reverseProxyRouteForHost(authority.host, config);
|
|
1731
|
+
if (!route?.targetUrl)
|
|
1732
|
+
return null;
|
|
1733
|
+
const url = new URL(route.targetUrl);
|
|
1734
|
+
if (url.protocol !== 'https:' && route.tlsMode !== 'passthrough')
|
|
1735
|
+
return null;
|
|
1736
|
+
return {
|
|
1737
|
+
host: stripHostBrackets(url.hostname),
|
|
1738
|
+
port: upstreamPort(url)
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
function shouldPreserveHostForGatewayRoute(route, url) {
|
|
1742
|
+
const dnsTarget = normalizeDnsTarget(route.dnsTarget);
|
|
1743
|
+
if (!dnsTarget || stripHostBrackets(url.hostname) !== dnsTarget)
|
|
1744
|
+
return false;
|
|
1745
|
+
return INTERNAL_GATEWAY_APP_PORTS.has(upstreamPort(url));
|
|
1746
|
+
}
|
|
1747
|
+
function upstreamPort(url) {
|
|
1748
|
+
const port = Number(url.port);
|
|
1749
|
+
if (Number.isInteger(port) && port > 0 && port <= 65535)
|
|
1750
|
+
return port;
|
|
1751
|
+
return url.protocol === 'https:' ? 443 : 80;
|
|
1752
|
+
}
|
|
1753
|
+
function reverseProxyPath(upstream, target) {
|
|
1754
|
+
const base = upstream.pathname && upstream.pathname !== '/'
|
|
1755
|
+
? upstream.pathname.replace(/\/+$/g, '')
|
|
1756
|
+
: '';
|
|
1757
|
+
const path = target.pathname || '/';
|
|
1758
|
+
return `${base}${path.startsWith('/') ? path : `/${path}`}${target.search}`;
|
|
1759
|
+
}
|
|
1760
|
+
function proxyHttpTarget(req) {
|
|
1761
|
+
const raw = req.url || '';
|
|
1762
|
+
try {
|
|
1763
|
+
if (/^https?:\/\//i.test(raw))
|
|
1764
|
+
return new URL(raw);
|
|
1765
|
+
const host = headerString(req.headers.host);
|
|
1766
|
+
if (!host)
|
|
1767
|
+
return null;
|
|
1768
|
+
return new URL(raw || '/', `http://${host}`);
|
|
1769
|
+
}
|
|
1770
|
+
catch {
|
|
1771
|
+
return null;
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
function proxyForwardHeaders(req, hostHeader, originalHost) {
|
|
1775
|
+
const out = {};
|
|
1776
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
1777
|
+
if (value === undefined)
|
|
1778
|
+
continue;
|
|
1779
|
+
const lower = key.toLowerCase();
|
|
1780
|
+
if ([
|
|
1781
|
+
'connection',
|
|
1782
|
+
'proxy-authorization',
|
|
1783
|
+
'proxy-connection',
|
|
1784
|
+
'proxy-authenticate',
|
|
1785
|
+
'te',
|
|
1786
|
+
'trailer',
|
|
1787
|
+
'transfer-encoding',
|
|
1788
|
+
'upgrade'
|
|
1789
|
+
].includes(lower))
|
|
1790
|
+
continue;
|
|
1791
|
+
out[key] = Array.isArray(value) ? value.map(String) : String(value);
|
|
1792
|
+
}
|
|
1793
|
+
out.Host = hostHeader;
|
|
1794
|
+
if (originalHost && originalHost !== hostHeader) {
|
|
1795
|
+
out['X-Forwarded-Host'] = originalHost;
|
|
1796
|
+
out['X-MX-Original-Host'] = originalHost;
|
|
1797
|
+
}
|
|
1798
|
+
return out;
|
|
1799
|
+
}
|
|
1800
|
+
function localControlPath(req) {
|
|
1801
|
+
const raw = req.url || '/';
|
|
1802
|
+
try {
|
|
1803
|
+
if (/^https?:\/\//i.test(raw)) {
|
|
1804
|
+
const url = new URL(raw);
|
|
1805
|
+
return isLocalhostName(url.hostname) ? url.pathname : null;
|
|
1806
|
+
}
|
|
1807
|
+
return new URL(raw, 'http://127.0.0.1').pathname;
|
|
1808
|
+
}
|
|
1809
|
+
catch {
|
|
1810
|
+
return null;
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
async function registerSharedLocalPacServer(port, config) {
|
|
1814
|
+
const pacUrl = `http://127.0.0.1:${port}${PAC_PATH}`;
|
|
1815
|
+
const status = await sharedLocalPacStatus(port).catch(() => null);
|
|
1816
|
+
if (status?.marker !== PAC_MARKER) {
|
|
1817
|
+
throw new Error(`127.0.0.1:${port} is already in use and is not an Electron Launcher local edge`);
|
|
1818
|
+
}
|
|
1819
|
+
const response = await httpJsonRequest(`http://127.0.0.1:${port}${SHARED_APPLY_PATH}`, config, 1800);
|
|
1820
|
+
if (response.marker !== PAC_MARKER) {
|
|
1821
|
+
throw new Error(`127.0.0.1:${port} did not accept Electron Launcher PAC registration`);
|
|
1822
|
+
}
|
|
1823
|
+
return { pacUrl, config: normalizeSharedLocalPacConfig(response) };
|
|
1824
|
+
}
|
|
1825
|
+
async function releaseSharedLocalPacServer(port, ownerId) {
|
|
1826
|
+
const response = await httpJsonRequest(`http://127.0.0.1:${port}${SHARED_RELEASE_PATH}`, { ownerId }, 1800);
|
|
1827
|
+
if (response.marker !== PAC_MARKER) {
|
|
1828
|
+
throw new Error(`127.0.0.1:${port} did not accept Electron Launcher PAC release`);
|
|
1829
|
+
}
|
|
1830
|
+
return response;
|
|
1831
|
+
}
|
|
1832
|
+
async function sharedLocalPacStatus(port) {
|
|
1833
|
+
return httpJsonRequest(`http://127.0.0.1:${port}${SHARED_STATUS_PATH}`, null, 1200);
|
|
1834
|
+
}
|
|
1835
|
+
function resolvedPacSourceFromLocalConfig(pacUrl, pacPort, systemResolverMode, config) {
|
|
1836
|
+
const effective = applyOwnershipRegistryToLocalConfig(config);
|
|
1837
|
+
return {
|
|
1838
|
+
pacUrl,
|
|
1839
|
+
domains: effective.domains,
|
|
1840
|
+
proxy: effective.proxy,
|
|
1841
|
+
matchMode: effective.matchMode,
|
|
1842
|
+
fallbackProxy: effective.fallbackProxy,
|
|
1843
|
+
pacPort,
|
|
1844
|
+
sharedLocalPac: false,
|
|
1845
|
+
dnsServers: effective.dnsServers,
|
|
1846
|
+
dnsFallbackTarget: effective.dnsFallbackTarget,
|
|
1847
|
+
systemResolverMode,
|
|
1848
|
+
reverseProxyRoutes: effective.reverseProxyRoutes,
|
|
1849
|
+
ownershipClaim: null,
|
|
1850
|
+
ownershipClaims: effective.ownershipClaims,
|
|
1851
|
+
usesLocalPac: true
|
|
1852
|
+
};
|
|
1853
|
+
}
|
|
1854
|
+
function normalizeSharedLocalPacConfig(value) {
|
|
1855
|
+
const row = value && typeof value === 'object' ? value : {};
|
|
1856
|
+
return {
|
|
1857
|
+
domains: normalizeDomains(row.domains),
|
|
1858
|
+
proxy: normalizeProxyAddress(row.proxyDirective) || normalizeProxyAddress(row.proxy),
|
|
1859
|
+
matchMode: normalizeMatchMode(row.matchMode, row.proxy),
|
|
1860
|
+
fallbackProxy: normalizeProxyAddress(row.fallbackProxyDirective) || normalizeProxyAddress(row.fallbackProxy),
|
|
1861
|
+
dnsServers: normalizeDnsServers(row.dnsServers),
|
|
1862
|
+
dnsFallbackTarget: normalizeDnsTarget(row.dnsFallbackTarget),
|
|
1863
|
+
reverseProxyRoutes: normalizeReverseProxyRoutes(row.reverseProxyRoutes),
|
|
1864
|
+
ownershipClaims: normalizeOwnershipClaims([
|
|
1865
|
+
...arrayValue(row.ownershipClaims, []),
|
|
1866
|
+
row.ownershipClaim
|
|
1867
|
+
])
|
|
1868
|
+
};
|
|
1869
|
+
}
|
|
1870
|
+
function mergeLocalPacConfigs(current, incoming) {
|
|
1871
|
+
if (!current)
|
|
1872
|
+
return applyOwnershipRegistryToLocalConfig(incoming);
|
|
1873
|
+
return applyOwnershipRegistryToLocalConfig({
|
|
1874
|
+
domains: uniqueList([...current.domains, ...incoming.domains]),
|
|
1875
|
+
proxy: incoming.proxy || current.proxy,
|
|
1876
|
+
matchMode: current.matchMode === 'proxy' || incoming.matchMode === 'proxy' ? 'proxy' : incoming.matchMode,
|
|
1877
|
+
fallbackProxy: incoming.fallbackProxy || current.fallbackProxy,
|
|
1878
|
+
dnsServers: uniqueList([...current.dnsServers, ...incoming.dnsServers]),
|
|
1879
|
+
dnsFallbackTarget: incoming.dnsFallbackTarget || current.dnsFallbackTarget,
|
|
1880
|
+
reverseProxyRoutes: mergeReverseProxyRoutes(current.reverseProxyRoutes, incoming.reverseProxyRoutes),
|
|
1881
|
+
ownershipClaims: mergeOwnershipClaims(current.ownershipClaims, incoming.ownershipClaims)
|
|
1882
|
+
});
|
|
1883
|
+
}
|
|
1884
|
+
function releaseLocalPacConfigOwner(config, ownerId) {
|
|
1885
|
+
const releasedOwnerId = normalizeOwnerId(ownerId);
|
|
1886
|
+
const claims = normalizeOwnershipClaims(config.ownershipClaims);
|
|
1887
|
+
const releasedClaims = claims.filter((claim) => normalizeOwnerId(claim.ownerId) === releasedOwnerId);
|
|
1888
|
+
const remainingClaims = claims.filter((claim) => normalizeOwnerId(claim.ownerId) !== releasedOwnerId);
|
|
1889
|
+
const releasedDomains = new Set(releasedClaims.flatMap(ownershipClaimDomains));
|
|
1890
|
+
const remainingDomains = new Set(remainingClaims.flatMap(ownershipClaimDomains));
|
|
1891
|
+
const releasedRouteHosts = new Set(releasedClaims.flatMap(ownershipClaimRouteHosts));
|
|
1892
|
+
const remainingRouteHosts = new Set(remainingClaims.flatMap(ownershipClaimRouteHosts));
|
|
1893
|
+
return applyOwnershipRegistryToLocalConfig({
|
|
1894
|
+
...config,
|
|
1895
|
+
domains: config.domains.filter((domain) => !releasedDomains.has(domain) || remainingDomains.has(domain)),
|
|
1896
|
+
reverseProxyRoutes: config.reverseProxyRoutes.filter((route) => {
|
|
1897
|
+
const host = normalizeDomainName(route.host);
|
|
1898
|
+
return !host || !releasedRouteHosts.has(host) || remainingRouteHosts.has(host);
|
|
1899
|
+
}),
|
|
1900
|
+
ownershipClaims: remainingClaims
|
|
1901
|
+
});
|
|
1902
|
+
}
|
|
1903
|
+
function applyOwnershipRegistryToLocalConfig(config) {
|
|
1904
|
+
const ownershipClaims = normalizeOwnershipClaims(config.ownershipClaims);
|
|
1905
|
+
if (!ownershipClaims.length) {
|
|
1906
|
+
return {
|
|
1907
|
+
...config,
|
|
1908
|
+
ownershipClaims
|
|
1909
|
+
};
|
|
1910
|
+
}
|
|
1911
|
+
const registry = buildElectronLauncherNetworkOwnershipRegistry(ownershipClaims);
|
|
1912
|
+
const registryZones = mergedElectronLauncherDnsZones(registry);
|
|
1913
|
+
const registryHosts = registry.dnsHosts.map((entry) => entry.key);
|
|
1914
|
+
const registryRoutes = mergedElectronLauncherReverseProxyRoutes(registry)
|
|
1915
|
+
.map((route) => ({
|
|
1916
|
+
routeId: route.routeId,
|
|
1917
|
+
host: route.host,
|
|
1918
|
+
dnsTarget: route.dnsTarget,
|
|
1919
|
+
targetUrl: route.targetUrl,
|
|
1920
|
+
tlsMode: normalizeTlsMode(route.tlsMode),
|
|
1921
|
+
authRequired: route.authRequired,
|
|
1922
|
+
enabled: route.enabled
|
|
1923
|
+
}));
|
|
1924
|
+
return {
|
|
1925
|
+
...config,
|
|
1926
|
+
domains: uniqueList([...config.domains, ...registryZones, ...registryHosts]),
|
|
1927
|
+
reverseProxyRoutes: mergeReverseProxyRoutes(config.reverseProxyRoutes, registryRoutes),
|
|
1928
|
+
ownershipClaims
|
|
1929
|
+
};
|
|
1930
|
+
}
|
|
1931
|
+
function localConfigOwnershipRegistry(config) {
|
|
1932
|
+
return buildElectronLauncherNetworkOwnershipRegistry(config.ownershipClaims);
|
|
1933
|
+
}
|
|
1934
|
+
function pacDirectCidrs(claims) {
|
|
1935
|
+
return uniqueList(normalizeOwnershipClaims(claims)
|
|
1936
|
+
.flatMap((claim) => claim.routeCidrs || [])
|
|
1937
|
+
.map(normalizeIpv4Cidr)
|
|
1938
|
+
.filter(Boolean))
|
|
1939
|
+
.map((cidr) => {
|
|
1940
|
+
const [base, prefix] = cidr.split('/');
|
|
1941
|
+
return {
|
|
1942
|
+
base,
|
|
1943
|
+
mask: prefixToIpv4Mask(Number(prefix))
|
|
1944
|
+
};
|
|
1945
|
+
});
|
|
1946
|
+
}
|
|
1947
|
+
function normalizeIpv4Cidr(value) {
|
|
1948
|
+
const text = stringValue(value);
|
|
1949
|
+
if (!text)
|
|
1950
|
+
return '';
|
|
1951
|
+
const [ip, rawPrefix] = text.split('/');
|
|
1952
|
+
const prefix = rawPrefix === undefined ? 32 : Number(rawPrefix);
|
|
1953
|
+
if (isIP(ip) !== 4 || !Number.isInteger(prefix) || prefix < 0 || prefix > 32)
|
|
1954
|
+
return '';
|
|
1955
|
+
return `${ip}/${prefix}`;
|
|
1956
|
+
}
|
|
1957
|
+
function prefixToIpv4Mask(prefix) {
|
|
1958
|
+
const mask = prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0;
|
|
1959
|
+
return [
|
|
1960
|
+
(mask >>> 24) & 255,
|
|
1961
|
+
(mask >>> 16) & 255,
|
|
1962
|
+
(mask >>> 8) & 255,
|
|
1963
|
+
mask & 255
|
|
1964
|
+
].join('.');
|
|
1965
|
+
}
|
|
1966
|
+
function requiresManagedRelease(state, config) {
|
|
1967
|
+
const ownerId = stateOwnershipOwnerId(state);
|
|
1968
|
+
if (!ownerId)
|
|
1969
|
+
return false;
|
|
1970
|
+
if (state.sharedLocalPac === true)
|
|
1971
|
+
return true;
|
|
1972
|
+
const claims = normalizeOwnershipClaims(config?.ownershipClaims);
|
|
1973
|
+
return claims.some((claim) => normalizeOwnerId(claim.ownerId) !== ownerId);
|
|
1974
|
+
}
|
|
1975
|
+
function stateOwnershipOwnerId(state) {
|
|
1976
|
+
return normalizeOwnerId(state.ownershipClaim?.ownerId);
|
|
1977
|
+
}
|
|
1978
|
+
function ownershipClaimDomains(claim) {
|
|
1979
|
+
return uniqueList([
|
|
1980
|
+
...normalizeDomains(claim.dnsZones),
|
|
1981
|
+
...normalizeDomains(claim.dnsHosts),
|
|
1982
|
+
...ownershipClaimRouteHosts(claim)
|
|
1983
|
+
]);
|
|
1984
|
+
}
|
|
1985
|
+
function ownershipClaimRouteHosts(claim) {
|
|
1986
|
+
return normalizeReverseProxyRoutes(claim.reverseProxyRoutes).map((route) => route.host);
|
|
1987
|
+
}
|
|
1988
|
+
function mergeOwnershipClaims(current, incoming) {
|
|
1989
|
+
const byOwner = new Map();
|
|
1990
|
+
for (const claim of normalizeOwnershipClaims(current))
|
|
1991
|
+
byOwner.set(claim.ownerId, claim);
|
|
1992
|
+
for (const claim of normalizeOwnershipClaims(incoming))
|
|
1993
|
+
byOwner.set(claim.ownerId, claim);
|
|
1994
|
+
return [...byOwner.values()];
|
|
1995
|
+
}
|
|
1996
|
+
function localPacConfigKey(config, port) {
|
|
1997
|
+
return JSON.stringify({
|
|
1998
|
+
domains: config.domains,
|
|
1999
|
+
matchMode: config.matchMode,
|
|
2000
|
+
proxy: config.proxy?.directive || null,
|
|
2001
|
+
fallbackProxy: config.fallbackProxy?.directive || null,
|
|
2002
|
+
dnsServers: config.dnsServers,
|
|
2003
|
+
dnsFallbackTarget: config.dnsFallbackTarget,
|
|
2004
|
+
reverseProxyRoutes: config.reverseProxyRoutes,
|
|
2005
|
+
ownershipClaims: config.ownershipClaims.map((claim) => claim.ownerId).sort(),
|
|
2006
|
+
directCidrs: pacDirectCidrs(config.ownershipClaims),
|
|
2007
|
+
pacPort: port || null
|
|
2008
|
+
});
|
|
2009
|
+
}
|
|
2010
|
+
function mergeReverseProxyRoutes(current, incoming) {
|
|
2011
|
+
const byHost = new Map();
|
|
2012
|
+
for (const route of current)
|
|
2013
|
+
byHost.set(route.host, route);
|
|
2014
|
+
for (const route of incoming)
|
|
2015
|
+
byHost.set(route.host, route);
|
|
2016
|
+
return [...byHost.values()];
|
|
2017
|
+
}
|
|
2018
|
+
function parseProxyAuthority(value, defaultPort) {
|
|
2019
|
+
const text = value.trim();
|
|
2020
|
+
if (!text)
|
|
2021
|
+
return null;
|
|
2022
|
+
const bracket = text.match(/^\[([^\]]+)](?::(\d+))?$/);
|
|
2023
|
+
if (bracket) {
|
|
2024
|
+
return normalizeProxyAuthority(bracket[1], bracket[2], defaultPort);
|
|
2025
|
+
}
|
|
2026
|
+
const parts = text.split(':');
|
|
2027
|
+
if (parts.length === 1)
|
|
2028
|
+
return normalizeProxyAuthority(parts[0], null, defaultPort);
|
|
2029
|
+
const port = parts.pop();
|
|
2030
|
+
return normalizeProxyAuthority(parts.join(':'), port || null, defaultPort);
|
|
2031
|
+
}
|
|
2032
|
+
function normalizeProxyAuthority(host, port, defaultPort) {
|
|
2033
|
+
const cleanHost = stripHostBrackets(host);
|
|
2034
|
+
if (!cleanHost)
|
|
2035
|
+
return null;
|
|
2036
|
+
const parsedPort = port ? Number(port) : defaultPort;
|
|
2037
|
+
if (!Number.isInteger(parsedPort) || parsedPort <= 0 || parsedPort > 65535)
|
|
2038
|
+
return null;
|
|
2039
|
+
return { host: cleanHost, port: parsedPort };
|
|
2040
|
+
}
|
|
2041
|
+
function destroyProxyClient(socket, response) {
|
|
2042
|
+
if (!socket.destroyed)
|
|
2043
|
+
socket.end(response);
|
|
2044
|
+
}
|
|
2045
|
+
function writeTextResponse(res, status, body) {
|
|
2046
|
+
res.writeHead(status, {
|
|
2047
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
2048
|
+
'Content-Length': String(Buffer.byteLength(body, 'utf8')),
|
|
2049
|
+
Connection: 'close'
|
|
2050
|
+
});
|
|
2051
|
+
res.end(body);
|
|
2052
|
+
}
|
|
2053
|
+
function writeJsonResponse(res, status, body) {
|
|
2054
|
+
const text = `${JSON.stringify(body)}\n`;
|
|
2055
|
+
res.writeHead(status, {
|
|
2056
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
2057
|
+
'Content-Length': String(Buffer.byteLength(text, 'utf8')),
|
|
2058
|
+
Connection: 'close'
|
|
2059
|
+
});
|
|
2060
|
+
res.end(text);
|
|
2061
|
+
}
|
|
2062
|
+
function readRequestJson(req) {
|
|
2063
|
+
return new Promise((resolve, reject) => {
|
|
2064
|
+
const chunks = [];
|
|
2065
|
+
let size = 0;
|
|
2066
|
+
req.on('data', (chunk) => {
|
|
2067
|
+
size += chunk.length;
|
|
2068
|
+
if (size > 32_768) {
|
|
2069
|
+
reject(new Error('request body too large'));
|
|
2070
|
+
req.destroy();
|
|
2071
|
+
return;
|
|
2072
|
+
}
|
|
2073
|
+
chunks.push(chunk);
|
|
2074
|
+
});
|
|
2075
|
+
req.on('end', () => {
|
|
2076
|
+
try {
|
|
2077
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}'));
|
|
2078
|
+
}
|
|
2079
|
+
catch (err) {
|
|
2080
|
+
reject(err);
|
|
2081
|
+
}
|
|
2082
|
+
});
|
|
2083
|
+
req.on('error', reject);
|
|
2084
|
+
});
|
|
2085
|
+
}
|
|
2086
|
+
function httpJsonRequest(url, body, timeoutMs) {
|
|
2087
|
+
return new Promise((resolve, reject) => {
|
|
2088
|
+
const parsed = new URL(url);
|
|
2089
|
+
const payload = body === null ? null : JSON.stringify(normalizeHttpJsonBody(body));
|
|
2090
|
+
const req = httpRequest({
|
|
2091
|
+
hostname: parsed.hostname,
|
|
2092
|
+
port: parsed.port,
|
|
2093
|
+
path: `${parsed.pathname}${parsed.search}`,
|
|
2094
|
+
method: payload === null ? 'GET' : 'POST',
|
|
2095
|
+
headers: payload === null ? undefined : {
|
|
2096
|
+
'Content-Type': 'application/json',
|
|
2097
|
+
'Content-Length': String(Buffer.byteLength(payload, 'utf8'))
|
|
2098
|
+
},
|
|
2099
|
+
timeout: timeoutMs
|
|
2100
|
+
}, (res) => {
|
|
2101
|
+
const chunks = [];
|
|
2102
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
2103
|
+
res.on('end', () => {
|
|
2104
|
+
const text = Buffer.concat(chunks).toString('utf8');
|
|
2105
|
+
if ((res.statusCode || 500) >= 400) {
|
|
2106
|
+
reject(new Error(text || `HTTP ${res.statusCode}`));
|
|
2107
|
+
return;
|
|
2108
|
+
}
|
|
2109
|
+
try {
|
|
2110
|
+
resolve(JSON.parse(text || '{}'));
|
|
2111
|
+
}
|
|
2112
|
+
catch (err) {
|
|
2113
|
+
reject(err);
|
|
2114
|
+
}
|
|
2115
|
+
});
|
|
2116
|
+
});
|
|
2117
|
+
req.on('timeout', () => req.destroy(new Error(`request timeout: ${url}`)));
|
|
2118
|
+
req.on('error', reject);
|
|
2119
|
+
if (payload !== null)
|
|
2120
|
+
req.end(payload);
|
|
2121
|
+
else
|
|
2122
|
+
req.end();
|
|
2123
|
+
});
|
|
2124
|
+
}
|
|
2125
|
+
function normalizeHttpJsonBody(body) {
|
|
2126
|
+
const row = body && typeof body === 'object' ? body : {};
|
|
2127
|
+
const ownerId = normalizeOwnerId(row.ownerId);
|
|
2128
|
+
if (ownerId && row.domains === undefined && row.ownershipClaims === undefined && row.proxy === undefined) {
|
|
2129
|
+
return { ownerId };
|
|
2130
|
+
}
|
|
2131
|
+
return {
|
|
2132
|
+
domains: normalizeDomains(body.domains),
|
|
2133
|
+
proxy: body.proxy?.address || null,
|
|
2134
|
+
proxyDirective: body.proxy?.directive || null,
|
|
2135
|
+
matchMode: body.matchMode,
|
|
2136
|
+
fallbackProxy: body.fallbackProxy?.address || null,
|
|
2137
|
+
fallbackProxyDirective: body.fallbackProxy?.directive || null,
|
|
2138
|
+
dnsServers: normalizeDnsServers(body.dnsServers),
|
|
2139
|
+
dnsFallbackTarget: normalizeDnsTarget(body.dnsFallbackTarget),
|
|
2140
|
+
reverseProxyRoutes: normalizeReverseProxyRoutes(body.reverseProxyRoutes),
|
|
2141
|
+
ownershipClaims: normalizeOwnershipClaims(body.ownershipClaims)
|
|
2142
|
+
};
|
|
2143
|
+
}
|
|
2144
|
+
function resolveDnsA(hostname, server) {
|
|
2145
|
+
const query = buildDnsAQuery(hostname);
|
|
2146
|
+
const endpoint = parseDnsServerEndpoint(server);
|
|
2147
|
+
if (!endpoint)
|
|
2148
|
+
return Promise.reject(new Error(`Invalid DNS server endpoint: ${server}`));
|
|
2149
|
+
return new Promise((resolve, reject) => {
|
|
2150
|
+
const socket = createSocket('udp4');
|
|
2151
|
+
const timer = setTimeout(() => {
|
|
2152
|
+
socket.close();
|
|
2153
|
+
reject(new Error(`DNS timeout for ${hostname} via ${server}`));
|
|
2154
|
+
}, DNS_QUERY_TIMEOUT_MS);
|
|
2155
|
+
socket.once('message', (message) => {
|
|
2156
|
+
clearTimeout(timer);
|
|
2157
|
+
socket.close();
|
|
2158
|
+
try {
|
|
2159
|
+
const address = parseDnsAResponse(message, query.id);
|
|
2160
|
+
if (!address)
|
|
2161
|
+
throw new Error(`DNS A record missing for ${hostname}`);
|
|
2162
|
+
resolve(address);
|
|
2163
|
+
}
|
|
2164
|
+
catch (err) {
|
|
2165
|
+
reject(err);
|
|
2166
|
+
}
|
|
2167
|
+
});
|
|
2168
|
+
socket.once('error', (err) => {
|
|
2169
|
+
clearTimeout(timer);
|
|
2170
|
+
socket.close();
|
|
2171
|
+
reject(err);
|
|
2172
|
+
});
|
|
2173
|
+
socket.send(query.packet, endpoint.port, endpoint.host);
|
|
2174
|
+
});
|
|
2175
|
+
}
|
|
2176
|
+
async function forwardDnsPacket(packet, servers) {
|
|
2177
|
+
const failures = [];
|
|
2178
|
+
for (const server of servers) {
|
|
2179
|
+
try {
|
|
2180
|
+
return await forwardDnsPacketToServer(packet, server);
|
|
2181
|
+
}
|
|
2182
|
+
catch (err) {
|
|
2183
|
+
failures.push(`${server}: ${errorMessage(err)}`);
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
throw new Error(failures.length ? failures.join('; ') : 'Internal DNS server is not configured');
|
|
2187
|
+
}
|
|
2188
|
+
function forwardDnsPacketToServer(packet, server) {
|
|
2189
|
+
const endpoint = parseDnsServerEndpoint(server);
|
|
2190
|
+
if (!endpoint)
|
|
2191
|
+
return Promise.reject(new Error(`Invalid DNS server endpoint: ${server}`));
|
|
2192
|
+
return new Promise((resolve, reject) => {
|
|
2193
|
+
const socket = createSocket('udp4');
|
|
2194
|
+
const timer = setTimeout(() => {
|
|
2195
|
+
socket.close();
|
|
2196
|
+
reject(new Error(`DNS relay timeout via ${server}`));
|
|
2197
|
+
}, DNS_QUERY_TIMEOUT_MS);
|
|
2198
|
+
socket.once('message', (message) => {
|
|
2199
|
+
clearTimeout(timer);
|
|
2200
|
+
socket.close();
|
|
2201
|
+
resolve(message);
|
|
2202
|
+
});
|
|
2203
|
+
socket.once('error', (err) => {
|
|
2204
|
+
clearTimeout(timer);
|
|
2205
|
+
socket.close();
|
|
2206
|
+
reject(err);
|
|
2207
|
+
});
|
|
2208
|
+
socket.send(packet, endpoint.port, endpoint.host);
|
|
2209
|
+
});
|
|
2210
|
+
}
|
|
2211
|
+
function dnsFailureResponse(query) {
|
|
2212
|
+
if (query.length < 12)
|
|
2213
|
+
return Buffer.alloc(0);
|
|
2214
|
+
const response = Buffer.from(query);
|
|
2215
|
+
const flags = query.readUInt16BE(2);
|
|
2216
|
+
response.writeUInt16BE((flags | 0x8000) & 0xfff0 | 0x0002, 2);
|
|
2217
|
+
response.writeUInt16BE(0, 6);
|
|
2218
|
+
response.writeUInt16BE(0, 8);
|
|
2219
|
+
response.writeUInt16BE(0, 10);
|
|
2220
|
+
return response;
|
|
2221
|
+
}
|
|
2222
|
+
function dnsAResponse(query, address) {
|
|
2223
|
+
if (query.length < 12 || isIP(address) !== 4)
|
|
2224
|
+
return Buffer.alloc(0);
|
|
2225
|
+
const questionEnd = dnsQuestionEndOffset(query);
|
|
2226
|
+
if (!questionEnd)
|
|
2227
|
+
return Buffer.alloc(0);
|
|
2228
|
+
const response = Buffer.alloc(questionEnd + 16);
|
|
2229
|
+
query.copy(response, 0, 0, questionEnd);
|
|
2230
|
+
const flags = query.readUInt16BE(2);
|
|
2231
|
+
response.writeUInt16BE((flags | 0x8000 | 0x0080) & 0xfff0, 2);
|
|
2232
|
+
response.writeUInt16BE(1, 6);
|
|
2233
|
+
response.writeUInt16BE(0, 8);
|
|
2234
|
+
response.writeUInt16BE(0, 10);
|
|
2235
|
+
let offset = questionEnd;
|
|
2236
|
+
response.writeUInt16BE(0xc00c, offset);
|
|
2237
|
+
offset += 2;
|
|
2238
|
+
response.writeUInt16BE(1, offset);
|
|
2239
|
+
offset += 2;
|
|
2240
|
+
response.writeUInt16BE(1, offset);
|
|
2241
|
+
offset += 2;
|
|
2242
|
+
response.writeUInt32BE(30, offset);
|
|
2243
|
+
offset += 4;
|
|
2244
|
+
response.writeUInt16BE(4, offset);
|
|
2245
|
+
offset += 2;
|
|
2246
|
+
for (const part of address.split('.')) {
|
|
2247
|
+
response[offset] = Number(part);
|
|
2248
|
+
offset += 1;
|
|
2249
|
+
}
|
|
2250
|
+
return response;
|
|
2251
|
+
}
|
|
2252
|
+
function dnsQuestionEndOffset(packet) {
|
|
2253
|
+
if (packet.length < 13 || packet.readUInt16BE(4) < 1)
|
|
2254
|
+
return null;
|
|
2255
|
+
let offset = 12;
|
|
2256
|
+
while (offset < packet.length) {
|
|
2257
|
+
const length = packet[offset];
|
|
2258
|
+
if (length === 0)
|
|
2259
|
+
return offset + 5 <= packet.length ? offset + 5 : null;
|
|
2260
|
+
if ((length & 0xc0) === 0xc0 || offset + 1 + length > packet.length)
|
|
2261
|
+
return null;
|
|
2262
|
+
offset += length + 1;
|
|
2263
|
+
}
|
|
2264
|
+
return null;
|
|
2265
|
+
}
|
|
2266
|
+
function dnsQuestionHost(packet) {
|
|
2267
|
+
if (packet.length < 13 || packet.readUInt16BE(4) < 1)
|
|
2268
|
+
return null;
|
|
2269
|
+
const labels = [];
|
|
2270
|
+
let offset = 12;
|
|
2271
|
+
while (offset < packet.length) {
|
|
2272
|
+
const length = packet[offset];
|
|
2273
|
+
if (length === 0)
|
|
2274
|
+
return labels.join('.').toLowerCase() || null;
|
|
2275
|
+
if ((length & 0xc0) === 0xc0 || offset + 1 + length > packet.length)
|
|
2276
|
+
return null;
|
|
2277
|
+
labels.push(packet.subarray(offset + 1, offset + 1 + length).toString('ascii'));
|
|
2278
|
+
offset += length + 1;
|
|
2279
|
+
}
|
|
2280
|
+
return null;
|
|
2281
|
+
}
|
|
2282
|
+
function buildDnsAQuery(hostname) {
|
|
2283
|
+
const id = Math.floor(Math.random() * 0xffff);
|
|
2284
|
+
const labels = hostname.split('.').filter(Boolean);
|
|
2285
|
+
const question = Buffer.concat([
|
|
2286
|
+
...labels.map((label) => {
|
|
2287
|
+
const bytes = Buffer.from(label, 'ascii');
|
|
2288
|
+
return Buffer.concat([Buffer.from([bytes.length]), bytes]);
|
|
2289
|
+
}),
|
|
2290
|
+
Buffer.from([0, 0, 1, 0, 1])
|
|
2291
|
+
]);
|
|
2292
|
+
const header = Buffer.alloc(12);
|
|
2293
|
+
header.writeUInt16BE(id, 0);
|
|
2294
|
+
header.writeUInt16BE(0x0100, 2);
|
|
2295
|
+
header.writeUInt16BE(1, 4);
|
|
2296
|
+
return { id, packet: Buffer.concat([header, question]) };
|
|
2297
|
+
}
|
|
2298
|
+
function parseDnsAResponse(packet, expectedId) {
|
|
2299
|
+
return parseDnsARecords(packet, expectedId)[0] || null;
|
|
2300
|
+
}
|
|
2301
|
+
function parseDnsARecords(packet, expectedId) {
|
|
2302
|
+
if (packet.length < 12 || packet.readUInt16BE(0) !== expectedId)
|
|
2303
|
+
return [];
|
|
2304
|
+
const rcode = packet.readUInt16BE(2) & 0x000f;
|
|
2305
|
+
if (rcode !== 0)
|
|
2306
|
+
throw new Error(`DNS response code ${rcode}`);
|
|
2307
|
+
const questions = packet.readUInt16BE(4);
|
|
2308
|
+
const answers = packet.readUInt16BE(6);
|
|
2309
|
+
let offset = 12;
|
|
2310
|
+
for (let i = 0; i < questions; i += 1) {
|
|
2311
|
+
offset = skipDnsName(packet, offset);
|
|
2312
|
+
offset += 4;
|
|
2313
|
+
}
|
|
2314
|
+
const records = [];
|
|
2315
|
+
for (let i = 0; i < answers; i += 1) {
|
|
2316
|
+
offset = skipDnsName(packet, offset);
|
|
2317
|
+
if (offset + 10 > packet.length)
|
|
2318
|
+
return [];
|
|
2319
|
+
const type = packet.readUInt16BE(offset);
|
|
2320
|
+
const klass = packet.readUInt16BE(offset + 2);
|
|
2321
|
+
const length = packet.readUInt16BE(offset + 8);
|
|
2322
|
+
offset += 10;
|
|
2323
|
+
if (offset + length > packet.length)
|
|
2324
|
+
return [];
|
|
2325
|
+
if (type === 1 && klass === 1 && length === 4) {
|
|
2326
|
+
records.push(`${packet[offset]}.${packet[offset + 1]}.${packet[offset + 2]}.${packet[offset + 3]}`);
|
|
2327
|
+
}
|
|
2328
|
+
offset += length;
|
|
2329
|
+
}
|
|
2330
|
+
return records;
|
|
2331
|
+
}
|
|
2332
|
+
function shouldUseDnsFallbackResponse(query, response, fallbackTarget) {
|
|
2333
|
+
if (!fallbackTarget || isIP(fallbackTarget) !== 4)
|
|
2334
|
+
return false;
|
|
2335
|
+
let records = [];
|
|
2336
|
+
try {
|
|
2337
|
+
records = parseDnsARecords(response, query.readUInt16BE(0));
|
|
2338
|
+
}
|
|
2339
|
+
catch {
|
|
2340
|
+
return true;
|
|
2341
|
+
}
|
|
2342
|
+
if (records.length === 0)
|
|
2343
|
+
return true;
|
|
2344
|
+
if (records.includes(fallbackTarget))
|
|
2345
|
+
return false;
|
|
2346
|
+
return records.every((record) => isPublicOrProxyFakeIp(record));
|
|
2347
|
+
}
|
|
2348
|
+
function isPublicOrProxyFakeIp(value) {
|
|
2349
|
+
if (isIP(value) !== 4)
|
|
2350
|
+
return false;
|
|
2351
|
+
return !isOverlayOrPrivateIp(value) || cidrContainsIp('198.18.0.0/15', value);
|
|
2352
|
+
}
|
|
2353
|
+
function isOverlayOrPrivateIp(value) {
|
|
2354
|
+
return [
|
|
2355
|
+
'10.0.0.0/8',
|
|
2356
|
+
'172.16.0.0/12',
|
|
2357
|
+
'192.168.0.0/16',
|
|
2358
|
+
'100.64.0.0/10',
|
|
2359
|
+
'127.0.0.0/8'
|
|
2360
|
+
].some((cidr) => cidrContainsIp(cidr, value));
|
|
2361
|
+
}
|
|
2362
|
+
function cidrContainsIp(cidr, ip) {
|
|
2363
|
+
const parsed = parseIpv4Cidr(cidr);
|
|
2364
|
+
const target = ipv4ToInt(ip);
|
|
2365
|
+
if (!parsed || target === null)
|
|
2366
|
+
return false;
|
|
2367
|
+
const mask = parsed.prefix === 0 ? 0 : (0xffffffff << (32 - parsed.prefix)) >>> 0;
|
|
2368
|
+
return (target & mask) === (parsed.base & mask);
|
|
2369
|
+
}
|
|
2370
|
+
function parseIpv4Cidr(value) {
|
|
2371
|
+
const [ip, prefixText] = value.split('/');
|
|
2372
|
+
const base = ipv4ToInt(ip);
|
|
2373
|
+
const prefix = Number(prefixText);
|
|
2374
|
+
if (base === null || !Number.isInteger(prefix) || prefix < 0 || prefix > 32)
|
|
2375
|
+
return null;
|
|
2376
|
+
return { base, prefix };
|
|
2377
|
+
}
|
|
2378
|
+
function ipv4ToInt(value) {
|
|
2379
|
+
const text = value?.trim();
|
|
2380
|
+
if (!text || isIP(text) !== 4)
|
|
2381
|
+
return null;
|
|
2382
|
+
const parts = text.split('.').map((part) => Number(part));
|
|
2383
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255))
|
|
2384
|
+
return null;
|
|
2385
|
+
return (((parts[0] * 256 + parts[1]) * 256 + parts[2]) * 256 + parts[3]) >>> 0;
|
|
2386
|
+
}
|
|
2387
|
+
function skipDnsName(packet, offset) {
|
|
2388
|
+
let cursor = offset;
|
|
2389
|
+
while (cursor < packet.length) {
|
|
2390
|
+
const length = packet[cursor];
|
|
2391
|
+
if ((length & 0xc0) === 0xc0)
|
|
2392
|
+
return cursor + 2;
|
|
2393
|
+
if (length === 0)
|
|
2394
|
+
return cursor + 1;
|
|
2395
|
+
cursor += length + 1;
|
|
2396
|
+
}
|
|
2397
|
+
return cursor;
|
|
2398
|
+
}
|
|
2399
|
+
function execFileText(command, args) {
|
|
2400
|
+
return new Promise((resolve, reject) => {
|
|
2401
|
+
execFile(command, args, { windowsHide: true }, (err, stdout, stderr) => {
|
|
2402
|
+
if (err) {
|
|
2403
|
+
reject(Object.assign(err, { stdout, stderr }));
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
2406
|
+
resolve({ stdout, stderr });
|
|
2407
|
+
});
|
|
2408
|
+
});
|
|
2409
|
+
}
|
|
2410
|
+
function readState(statePath) {
|
|
2411
|
+
try {
|
|
2412
|
+
const state = JSON.parse(readFileSync(statePath, 'utf8'));
|
|
2413
|
+
return state && state.version === STATE_VERSION ? state : null;
|
|
2414
|
+
}
|
|
2415
|
+
catch {
|
|
2416
|
+
return null;
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
function writeState(statePath, state) {
|
|
2420
|
+
mkdirSync(dirname(statePath), { recursive: true });
|
|
2421
|
+
writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`);
|
|
2422
|
+
}
|
|
2423
|
+
function removeState(statePath, log) {
|
|
2424
|
+
try {
|
|
2425
|
+
rmSync(statePath, { force: true });
|
|
2426
|
+
}
|
|
2427
|
+
catch (err) {
|
|
2428
|
+
log.warn('[electron-launcher] failed to remove system domain proxy state', err);
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
function systemDomainProxyStateChanged(existing, next) {
|
|
2432
|
+
return !existing
|
|
2433
|
+
|| existing.pacUrl !== next.pacUrl
|
|
2434
|
+
|| storedSystemResolverMode(existing) !== next.systemResolverMode
|
|
2435
|
+
|| existing.resolverPort !== next.resolverPort
|
|
2436
|
+
|| JSON.stringify(normalizeDomains(existing.resolverDomains)) !== JSON.stringify(normalizeDomains(next.resolverDomains));
|
|
2437
|
+
}
|
|
2438
|
+
function publicState(state, extra = {}) {
|
|
2439
|
+
return {
|
|
2440
|
+
supported: true,
|
|
2441
|
+
applied: state.applied === true,
|
|
2442
|
+
platform: state.platform,
|
|
2443
|
+
pacUrl: state.pacUrl || null,
|
|
2444
|
+
proxy: state.proxy || null,
|
|
2445
|
+
matchMode: state.matchMode || null,
|
|
2446
|
+
fallbackProxy: state.fallbackProxy || null,
|
|
2447
|
+
pacPort: state.pacPort || null,
|
|
2448
|
+
sharedPac: state.sharedLocalPac === true,
|
|
2449
|
+
dnsServers: normalizeDnsServers(state.dnsServers),
|
|
2450
|
+
dnsFallbackTarget: normalizeDnsTarget(state.dnsFallbackTarget),
|
|
2451
|
+
systemResolver: storedSystemResolverMode(state) !== 'off',
|
|
2452
|
+
systemResolverMode: storedSystemResolverMode(state),
|
|
2453
|
+
resolverDomains: normalizeDomains(state.resolverDomains),
|
|
2454
|
+
resolverPort: normalizePort(state.resolverPort),
|
|
2455
|
+
resolverApplied: state.resolverApplied === true,
|
|
2456
|
+
resolverError: state.resolverError || null,
|
|
2457
|
+
domains: normalizeDomains(state.domains),
|
|
2458
|
+
reverseProxyRoutes: normalizeReverseProxyRoutes(state.reverseProxyRoutes),
|
|
2459
|
+
ownershipRegistry: state.ownershipRegistry || (state.ownershipClaim
|
|
2460
|
+
? buildElectronLauncherNetworkOwnershipRegistry([state.ownershipClaim])
|
|
2461
|
+
: null),
|
|
2462
|
+
updatedAt: state.updatedAt || null,
|
|
2463
|
+
...extra
|
|
2464
|
+
};
|
|
2465
|
+
}
|
|
2466
|
+
function normalizeDomains(value) {
|
|
2467
|
+
if (!Array.isArray(value))
|
|
2468
|
+
return [];
|
|
2469
|
+
return [...new Set(value
|
|
2470
|
+
.map((item) => typeof item === 'string' ? item.trim().toLowerCase().replace(/^\.+|\.+$/g, '') : '')
|
|
2471
|
+
.filter(Boolean))];
|
|
2472
|
+
}
|
|
2473
|
+
function uniqueList(values) {
|
|
2474
|
+
return [...new Set(values.filter(Boolean))];
|
|
2475
|
+
}
|
|
2476
|
+
function arrayValue(value, fallback = []) {
|
|
2477
|
+
return Array.isArray(value) ? value : fallback;
|
|
2478
|
+
}
|
|
2479
|
+
function normalizeOwnershipClaims(value) {
|
|
2480
|
+
return arrayValue(value, [])
|
|
2481
|
+
.map(normalizeOwnershipClaim)
|
|
2482
|
+
.filter((claim) => Boolean(claim));
|
|
2483
|
+
}
|
|
2484
|
+
function normalizeOwnershipClaim(value) {
|
|
2485
|
+
const row = value && typeof value === 'object' ? value : null;
|
|
2486
|
+
if (!row)
|
|
2487
|
+
return null;
|
|
2488
|
+
const ownerId = normalizeOwnerId(row.ownerId);
|
|
2489
|
+
if (!ownerId)
|
|
2490
|
+
return null;
|
|
2491
|
+
return {
|
|
2492
|
+
ownerId,
|
|
2493
|
+
productId: stringValue(row.productId) || null,
|
|
2494
|
+
instanceId: stringValue(row.instanceId) || null,
|
|
2495
|
+
displayName: stringValue(row.displayName) || null,
|
|
2496
|
+
state: normalizeOwnershipState(row.state),
|
|
2497
|
+
priority: normalizeInteger(row.priority),
|
|
2498
|
+
leaseIp: normalizeDnsTarget(row.leaseIp),
|
|
2499
|
+
gatewayIp: normalizeDnsTarget(row.gatewayIp),
|
|
2500
|
+
dnsHosts: normalizeDomains(row.dnsHosts),
|
|
2501
|
+
dnsZones: normalizeDomains(row.dnsZones),
|
|
2502
|
+
routeCidrs: arrayValue(row.routeCidrs, [])
|
|
2503
|
+
.map((item) => stringValue(item))
|
|
2504
|
+
.filter((item) => Boolean(item)),
|
|
2505
|
+
reverseProxyRoutes: normalizeReverseProxyRoutes(row.reverseProxyRoutes),
|
|
2506
|
+
metadata: row.metadata && typeof row.metadata === 'object' ? row.metadata : null,
|
|
2507
|
+
updatedAt: stringValue(row.updatedAt) || null
|
|
2508
|
+
};
|
|
2509
|
+
}
|
|
2510
|
+
function normalizeOwnerId(value) {
|
|
2511
|
+
const text = stringValue(value);
|
|
2512
|
+
if (!text)
|
|
2513
|
+
return null;
|
|
2514
|
+
return text.replace(/[^a-zA-Z0-9_.:-]/g, '-').slice(0, 160) || null;
|
|
2515
|
+
}
|
|
2516
|
+
function normalizeOwnershipState(value) {
|
|
2517
|
+
return value === 'connecting' || value === 'active' || value === 'stale' || value === 'released'
|
|
2518
|
+
? value
|
|
2519
|
+
: 'active';
|
|
2520
|
+
}
|
|
2521
|
+
function normalizeInteger(value) {
|
|
2522
|
+
const number = typeof value === 'number' ? value : Number(value);
|
|
2523
|
+
return Number.isFinite(number) ? Math.trunc(number) : null;
|
|
2524
|
+
}
|
|
2525
|
+
function normalizeDnsServers(value) {
|
|
2526
|
+
const raw = Array.isArray(value)
|
|
2527
|
+
? value
|
|
2528
|
+
: (typeof value === 'string' ? value.split(/[,\s]+/) : []);
|
|
2529
|
+
return [...new Set(raw
|
|
2530
|
+
.map((item) => normalizeDnsServerEndpoint(item))
|
|
2531
|
+
.filter((item) => Boolean(item)))];
|
|
2532
|
+
}
|
|
2533
|
+
function normalizeDnsServerEndpoint(value) {
|
|
2534
|
+
const parsed = parseDnsServerEndpoint(value);
|
|
2535
|
+
if (!parsed)
|
|
2536
|
+
return null;
|
|
2537
|
+
if (parsed.port === 53)
|
|
2538
|
+
return parsed.host;
|
|
2539
|
+
return parsed.host.includes(':') ? `[${parsed.host}]:${parsed.port}` : `${parsed.host}:${parsed.port}`;
|
|
2540
|
+
}
|
|
2541
|
+
function parseDnsServerEndpoint(value) {
|
|
2542
|
+
const text = stringValue(value);
|
|
2543
|
+
if (!text)
|
|
2544
|
+
return null;
|
|
2545
|
+
let host = text;
|
|
2546
|
+
let portText = null;
|
|
2547
|
+
if (text.startsWith('[')) {
|
|
2548
|
+
const close = text.indexOf(']');
|
|
2549
|
+
if (close <= 0)
|
|
2550
|
+
return null;
|
|
2551
|
+
host = text.slice(1, close);
|
|
2552
|
+
const rest = text.slice(close + 1);
|
|
2553
|
+
if (rest.startsWith(':'))
|
|
2554
|
+
portText = rest.slice(1);
|
|
2555
|
+
}
|
|
2556
|
+
else {
|
|
2557
|
+
const portMatch = text.match(/^(.+):(\d{1,5})$/);
|
|
2558
|
+
if (portMatch && !portMatch[1].includes(':')) {
|
|
2559
|
+
host = portMatch[1];
|
|
2560
|
+
portText = portMatch[2];
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
const cleanHost = stripHostBrackets(host.trim().replace(/\.+$/g, ''));
|
|
2564
|
+
if (!isDnsServerHost(cleanHost))
|
|
2565
|
+
return null;
|
|
2566
|
+
const port = portText ? Number(portText) : 53;
|
|
2567
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535)
|
|
2568
|
+
return null;
|
|
2569
|
+
return { host: cleanHost, port };
|
|
2570
|
+
}
|
|
2571
|
+
function isDnsServerHost(host) {
|
|
2572
|
+
if (!host)
|
|
2573
|
+
return false;
|
|
2574
|
+
if (isIP(host))
|
|
2575
|
+
return true;
|
|
2576
|
+
if (host === 'localhost')
|
|
2577
|
+
return true;
|
|
2578
|
+
return /^(?=.{1,253}$)(?!-)[a-z0-9-]+(?:\.(?!-)[a-z0-9-]+)*\.?$/i.test(host);
|
|
2579
|
+
}
|
|
2580
|
+
function normalizeReverseProxyRoutes(value) {
|
|
2581
|
+
const rows = Array.isArray(value) ? value : [];
|
|
2582
|
+
const routes = [];
|
|
2583
|
+
const seen = new Set();
|
|
2584
|
+
for (const item of rows) {
|
|
2585
|
+
if (!item || typeof item !== 'object')
|
|
2586
|
+
continue;
|
|
2587
|
+
const row = item;
|
|
2588
|
+
if (row.enabled === false)
|
|
2589
|
+
continue;
|
|
2590
|
+
const host = normalizeDomainName(row.host);
|
|
2591
|
+
if (!host || seen.has(host))
|
|
2592
|
+
continue;
|
|
2593
|
+
const dnsTarget = normalizeDnsTarget(row.dnsTarget);
|
|
2594
|
+
const targetUrl = normalizeTargetUrl(row.targetUrl);
|
|
2595
|
+
routes.push({
|
|
2596
|
+
routeId: stringValue(row.routeId),
|
|
2597
|
+
host,
|
|
2598
|
+
dnsTarget,
|
|
2599
|
+
targetUrl,
|
|
2600
|
+
tlsMode: normalizeTlsMode(row.tlsMode),
|
|
2601
|
+
authRequired: row.authRequired === true,
|
|
2602
|
+
enabled: true
|
|
2603
|
+
});
|
|
2604
|
+
seen.add(host);
|
|
2605
|
+
}
|
|
2606
|
+
return routes;
|
|
2607
|
+
}
|
|
2608
|
+
function normalizeDomainName(value) {
|
|
2609
|
+
const text = stringValue(value)?.toLowerCase().replace(/^\.+|\.+$/g, '') || null;
|
|
2610
|
+
if (!text || text.length > 253 || text.includes('/') || text.includes(':'))
|
|
2611
|
+
return null;
|
|
2612
|
+
return text;
|
|
2613
|
+
}
|
|
2614
|
+
function normalizeDnsTarget(value) {
|
|
2615
|
+
const text = stringValue(value);
|
|
2616
|
+
if (!text)
|
|
2617
|
+
return null;
|
|
2618
|
+
const host = text.startsWith('[')
|
|
2619
|
+
? text.slice(1, text.indexOf(']') > 0 ? text.indexOf(']') : undefined)
|
|
2620
|
+
: text.split(':')[0];
|
|
2621
|
+
const cleanHost = stripHostBrackets(host || text);
|
|
2622
|
+
return isIP(cleanHost) ? cleanHost : null;
|
|
2623
|
+
}
|
|
2624
|
+
function normalizeTargetUrl(value) {
|
|
2625
|
+
const text = stringValue(value);
|
|
2626
|
+
if (!text)
|
|
2627
|
+
return null;
|
|
2628
|
+
try {
|
|
2629
|
+
const url = new URL(text);
|
|
2630
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:')
|
|
2631
|
+
return null;
|
|
2632
|
+
return url.toString();
|
|
2633
|
+
}
|
|
2634
|
+
catch {
|
|
2635
|
+
return null;
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
function normalizeTlsMode(value) {
|
|
2639
|
+
return value === 'passthrough' || value === 'edge-terminated' || value === 'internal' ? value : null;
|
|
2640
|
+
}
|
|
2641
|
+
function normalizePort(value) {
|
|
2642
|
+
const port = typeof value === 'number' ? value : Number.NaN;
|
|
2643
|
+
return Number.isInteger(port) && port > 0 && port <= 65535 ? port : null;
|
|
2644
|
+
}
|
|
2645
|
+
function normalizeSystemResolverMode(value) {
|
|
2646
|
+
if (value === true || value === 'dynamic')
|
|
2647
|
+
return 'dynamic';
|
|
2648
|
+
if (value === 'file')
|
|
2649
|
+
return 'file';
|
|
2650
|
+
return 'off';
|
|
2651
|
+
}
|
|
2652
|
+
function storedSystemResolverMode(state) {
|
|
2653
|
+
const mode = normalizeSystemResolverMode(state.systemResolverMode);
|
|
2654
|
+
if (mode !== 'off')
|
|
2655
|
+
return mode;
|
|
2656
|
+
if (state.systemResolver === true)
|
|
2657
|
+
return 'dynamic';
|
|
2658
|
+
if (normalizeDomains(state.resolverDomains).length > 0 || normalizePort(state.resolverPort))
|
|
2659
|
+
return 'file';
|
|
2660
|
+
return 'off';
|
|
2661
|
+
}
|
|
2662
|
+
function normalizeMatchMode(value, proxy) {
|
|
2663
|
+
if (value === 'proxy')
|
|
2664
|
+
return 'proxy';
|
|
2665
|
+
if (value === 'direct')
|
|
2666
|
+
return 'direct';
|
|
2667
|
+
return stringValue(proxy) ? 'proxy' : 'direct';
|
|
2668
|
+
}
|
|
2669
|
+
function stringValue(value) {
|
|
2670
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
2671
|
+
}
|
|
2672
|
+
function normalizeProxyAddress(value) {
|
|
2673
|
+
const text = stringValue(value);
|
|
2674
|
+
if (!text)
|
|
2675
|
+
return null;
|
|
2676
|
+
const parsed = parseProxyAddress(text);
|
|
2677
|
+
if (!parsed)
|
|
2678
|
+
return null;
|
|
2679
|
+
return {
|
|
2680
|
+
address: parsed.address,
|
|
2681
|
+
directive: `${parsed.kind === 'socks' ? 'SOCKS' : 'PROXY'} ${parsed.address}`
|
|
2682
|
+
};
|
|
2683
|
+
}
|
|
2684
|
+
function parseProxyAddress(value) {
|
|
2685
|
+
const text = value.trim();
|
|
2686
|
+
const withProtocol = text.match(/^(https?|socks5?|socks):\/\/(.+)$/i);
|
|
2687
|
+
const kind = withProtocol && /^socks/i.test(withProtocol[1]) ? 'socks' : 'http';
|
|
2688
|
+
const candidate = withProtocol ? withProtocol[2] : text;
|
|
2689
|
+
const match = candidate.match(/^(127\.0\.0\.1|localhost):([1-9]\d{0,4})$/i);
|
|
2690
|
+
if (!match)
|
|
2691
|
+
return null;
|
|
2692
|
+
const port = Number(match[2]);
|
|
2693
|
+
if (!Number.isInteger(port) || port > 65535)
|
|
2694
|
+
return null;
|
|
2695
|
+
return {
|
|
2696
|
+
kind,
|
|
2697
|
+
address: `${match[1]}:${port}`
|
|
2698
|
+
};
|
|
2699
|
+
}
|
|
2700
|
+
function hostMatchesDomains(host, domains) {
|
|
2701
|
+
const h = host.toLowerCase().replace(/\.+$/g, '');
|
|
2702
|
+
return normalizeDomains(domains).some((domain) => h === domain || h.endsWith(`.${domain}`));
|
|
2703
|
+
}
|
|
2704
|
+
function headerString(value) {
|
|
2705
|
+
if (Array.isArray(value))
|
|
2706
|
+
return value[0] || null;
|
|
2707
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
2708
|
+
}
|
|
2709
|
+
function stripHostBrackets(value) {
|
|
2710
|
+
return value.replace(/^\[|\]$/g, '').trim();
|
|
2711
|
+
}
|
|
2712
|
+
function isLocalhostName(value) {
|
|
2713
|
+
return /^(127\.0\.0\.1|localhost|\[?::1]?)$/i.test(value);
|
|
2714
|
+
}
|
|
2715
|
+
function isAddressInUseError(err) {
|
|
2716
|
+
return Boolean(err && typeof err === 'object' && err.code === 'EADDRINUSE');
|
|
2717
|
+
}
|
|
2718
|
+
function shellQuote(value) {
|
|
2719
|
+
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
2720
|
+
}
|
|
2721
|
+
function errorMessage(err) {
|
|
2722
|
+
return err instanceof Error ? err.message : String(err);
|
|
2723
|
+
}
|
|
2724
|
+
function darwinAuthorizationErrorMessage(err) {
|
|
2725
|
+
const message = errorMessage(err);
|
|
2726
|
+
return /用户已取消|\(-128\)|user canceled|user cancelled/i.test(message)
|
|
2727
|
+
? 'macOS administrator authorization canceled'
|
|
2728
|
+
: message;
|
|
2729
|
+
}
|