portless 0.15.2 → 0.15.4
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 +7 -3
- package/dist/{chunk-T7CGCIRO.js → chunk-JSJUKQRJ.js} +34 -7
- package/dist/cli.js +452 -302
- package/dist/index.d.ts +13 -1
- package/dist/index.js +3 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -16,9 +16,10 @@ import {
|
|
|
16
16
|
isProcessAlive,
|
|
17
17
|
parseHostname,
|
|
18
18
|
parseHostnames,
|
|
19
|
+
resolveUserHome,
|
|
19
20
|
shouldAutoSyncHosts,
|
|
20
21
|
syncHostsFile
|
|
21
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-JSJUKQRJ.js";
|
|
22
23
|
|
|
23
24
|
// src/colors.ts
|
|
24
25
|
function supportsColor() {
|
|
@@ -44,18 +45,96 @@ var gray = dim;
|
|
|
44
45
|
var colors_default = { bold, dim, red, green, yellow, blue, cyan, white, gray };
|
|
45
46
|
|
|
46
47
|
// src/cli.ts
|
|
47
|
-
import * as
|
|
48
|
+
import * as fs10 from "fs";
|
|
48
49
|
import * as path9 from "path";
|
|
49
50
|
import { spawn as spawn4, spawnSync as spawnSync5 } from "child_process";
|
|
50
51
|
import { StringDecoder } from "string_decoder";
|
|
51
52
|
|
|
52
53
|
// src/certs.ts
|
|
53
|
-
import * as
|
|
54
|
+
import * as fs2 from "fs";
|
|
54
55
|
import * as path from "path";
|
|
55
|
-
import * as
|
|
56
|
+
import * as crypto2 from "crypto";
|
|
56
57
|
import * as tls from "tls";
|
|
57
|
-
import { execFile as execFileCb, execFileSync } from "child_process";
|
|
58
|
+
import { execFile as execFileCb, execFileSync as execFileSync2 } from "child_process";
|
|
58
59
|
import { promisify } from "util";
|
|
60
|
+
|
|
61
|
+
// src/windows-ca.ts
|
|
62
|
+
import * as crypto from "crypto";
|
|
63
|
+
import * as fs from "fs";
|
|
64
|
+
import * as os from "os";
|
|
65
|
+
import { execFileSync } from "child_process";
|
|
66
|
+
var WINDOWS_COMMAND_TIMEOUT_MS = 3e4;
|
|
67
|
+
var commandOptions = {
|
|
68
|
+
encoding: "utf-8",
|
|
69
|
+
timeout: WINDOWS_COMMAND_TIMEOUT_MS,
|
|
70
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
71
|
+
};
|
|
72
|
+
var defaultRunner = (command, args, options) => execFileSync(command, args, options);
|
|
73
|
+
function isWSL(options = {}) {
|
|
74
|
+
const platform = options.platform ?? process.platform;
|
|
75
|
+
if (platform !== "linux") return false;
|
|
76
|
+
const env = options.env ?? process.env;
|
|
77
|
+
if (env.WSL_DISTRO_NAME || env.WSL_INTEROP) return true;
|
|
78
|
+
const release2 = options.release ?? os.release();
|
|
79
|
+
return release2.toLowerCase().includes("microsoft");
|
|
80
|
+
}
|
|
81
|
+
function wslWindowsCAStoreOptions(run = defaultRunner) {
|
|
82
|
+
const command = run(
|
|
83
|
+
"wslpath",
|
|
84
|
+
["-u", String.raw`C:\Windows\System32\certutil.exe`],
|
|
85
|
+
commandOptions
|
|
86
|
+
).trim();
|
|
87
|
+
return {
|
|
88
|
+
command,
|
|
89
|
+
certificatePath: (certificatePath) => run("wslpath", ["-w", certificatePath], commandOptions).trim(),
|
|
90
|
+
run
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function certificateFingerprint(certificatePath) {
|
|
94
|
+
const certificate = new crypto.X509Certificate(fs.readFileSync(certificatePath));
|
|
95
|
+
return certificate.fingerprint.replace(/:/g, "").toLowerCase();
|
|
96
|
+
}
|
|
97
|
+
function storeOptions(options) {
|
|
98
|
+
return {
|
|
99
|
+
command: options.command ?? "certutil",
|
|
100
|
+
certificatePath: options.certificatePath ?? ((certificatePath) => certificatePath),
|
|
101
|
+
run: options.run ?? defaultRunner
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function storeContainsFingerprint(fingerprint, resolved) {
|
|
105
|
+
const listing = resolved.run(resolved.command, ["-store", "-user", "Root"], commandOptions);
|
|
106
|
+
return listing.replace(/\s/g, "").toLowerCase().includes(fingerprint);
|
|
107
|
+
}
|
|
108
|
+
function isWindowsCATrusted(caCertPath, options = {}) {
|
|
109
|
+
try {
|
|
110
|
+
const resolved = storeOptions(options);
|
|
111
|
+
const fingerprint = certificateFingerprint(caCertPath);
|
|
112
|
+
return storeContainsFingerprint(fingerprint, resolved);
|
|
113
|
+
} catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function trustWindowsCA(caCertPath, options = {}) {
|
|
118
|
+
const resolved = storeOptions(options);
|
|
119
|
+
resolved.run(
|
|
120
|
+
resolved.command,
|
|
121
|
+
["-addstore", "-user", "Root", resolved.certificatePath(caCertPath)],
|
|
122
|
+
commandOptions
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
function untrustWindowsCA(caCertPath, options = {}) {
|
|
126
|
+
try {
|
|
127
|
+
const resolved = storeOptions(options);
|
|
128
|
+
const fingerprint = certificateFingerprint(caCertPath);
|
|
129
|
+
if (!storeContainsFingerprint(fingerprint, resolved)) return { removed: true };
|
|
130
|
+
resolved.run(resolved.command, ["-delstore", "-user", "Root", fingerprint], commandOptions);
|
|
131
|
+
return storeContainsFingerprint(fingerprint, resolved) ? { removed: false, error: "certutil could not remove the portless CA from Root" } : { removed: true };
|
|
132
|
+
} catch (error) {
|
|
133
|
+
return { removed: false, error: error instanceof Error ? error.message : String(error) };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/certs.ts
|
|
59
138
|
var CA_VALIDITY_DAYS = 3650;
|
|
60
139
|
var SERVER_VALIDITY_DAYS = 365;
|
|
61
140
|
var EXPIRY_BUFFER_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
@@ -69,9 +148,10 @@ var CA_CERT_FILE = "ca.pem";
|
|
|
69
148
|
var SERVER_KEY_FILE = "server-key.pem";
|
|
70
149
|
var SERVER_CERT_FILE = "server.pem";
|
|
71
150
|
var CA_TRUST_MARKER = "ca.trusted";
|
|
151
|
+
var CA_TRUST_REFRESH_PENDING = "ca.trust-refresh-pending";
|
|
72
152
|
function fileExists(filePath) {
|
|
73
153
|
try {
|
|
74
|
-
|
|
154
|
+
fs2.accessSync(filePath, fs2.constants.R_OK);
|
|
75
155
|
return true;
|
|
76
156
|
} catch {
|
|
77
157
|
return false;
|
|
@@ -80,30 +160,38 @@ function fileExists(filePath) {
|
|
|
80
160
|
function caFingerprint(stateDir) {
|
|
81
161
|
const caCertPath = path.join(stateDir, CA_CERT_FILE);
|
|
82
162
|
try {
|
|
83
|
-
const pem =
|
|
84
|
-
return
|
|
163
|
+
const pem = fs2.readFileSync(caCertPath);
|
|
164
|
+
return crypto2.createHash("sha256").update(pem).digest("hex");
|
|
85
165
|
} catch {
|
|
86
166
|
return null;
|
|
87
167
|
}
|
|
88
168
|
}
|
|
89
169
|
function readTrustMarker(stateDir) {
|
|
90
170
|
try {
|
|
91
|
-
const value =
|
|
171
|
+
const value = fs2.readFileSync(path.join(stateDir, CA_TRUST_MARKER), "utf-8").trim();
|
|
92
172
|
return value || null;
|
|
93
173
|
} catch {
|
|
94
174
|
return null;
|
|
95
175
|
}
|
|
96
176
|
}
|
|
177
|
+
function clearTrustRefreshPending(stateDir) {
|
|
178
|
+
try {
|
|
179
|
+
fs2.unlinkSync(path.join(stateDir, CA_TRUST_REFRESH_PENDING));
|
|
180
|
+
} catch {
|
|
181
|
+
}
|
|
182
|
+
}
|
|
97
183
|
function writeTrustMarker(stateDir) {
|
|
98
184
|
const fp = caFingerprint(stateDir);
|
|
99
185
|
if (fp) {
|
|
100
|
-
|
|
186
|
+
clearTrustRefreshPending(stateDir);
|
|
187
|
+
const marker = isWSL() ? `wsl:${fp}` : fp;
|
|
188
|
+
fs2.writeFileSync(path.join(stateDir, CA_TRUST_MARKER), marker + "\n");
|
|
101
189
|
fixOwnership(path.join(stateDir, CA_TRUST_MARKER));
|
|
102
190
|
}
|
|
103
191
|
}
|
|
104
192
|
function clearTrustMarker(stateDir) {
|
|
105
193
|
try {
|
|
106
|
-
|
|
194
|
+
fs2.unlinkSync(path.join(stateDir, CA_TRUST_MARKER));
|
|
107
195
|
} catch {
|
|
108
196
|
}
|
|
109
197
|
}
|
|
@@ -143,8 +231,8 @@ function opensslErrorMessage() {
|
|
|
143
231
|
}
|
|
144
232
|
function isCertValid(certPath) {
|
|
145
233
|
try {
|
|
146
|
-
const pem =
|
|
147
|
-
const cert = new
|
|
234
|
+
const pem = fs2.readFileSync(certPath, "utf-8");
|
|
235
|
+
const cert = new crypto2.X509Certificate(pem);
|
|
148
236
|
const expiry = new Date(cert.validTo).getTime();
|
|
149
237
|
return Date.now() + EXPIRY_BUFFER_MS < expiry;
|
|
150
238
|
} catch {
|
|
@@ -173,7 +261,7 @@ function isCertSignatureStrong(certPath) {
|
|
|
173
261
|
function openssl(args, options) {
|
|
174
262
|
try {
|
|
175
263
|
const extraEnv = getOpensslEnv();
|
|
176
|
-
return
|
|
264
|
+
return execFileSync2("openssl", args, {
|
|
177
265
|
encoding: "utf-8",
|
|
178
266
|
timeout: OPENSSL_TIMEOUT_MS,
|
|
179
267
|
input: options?.input,
|
|
@@ -226,8 +314,8 @@ function generateCA(stateDir) {
|
|
|
226
314
|
"-addext",
|
|
227
315
|
"keyUsage=critical,keyCertSign,cRLSign"
|
|
228
316
|
]);
|
|
229
|
-
|
|
230
|
-
|
|
317
|
+
fs2.chmodSync(keyPath, 384);
|
|
318
|
+
fs2.chmodSync(certPath, 420);
|
|
231
319
|
fixOwnership(keyPath, certPath);
|
|
232
320
|
clearTrustMarker(stateDir);
|
|
233
321
|
return { certPath, keyPath };
|
|
@@ -242,7 +330,7 @@ function generateServerCert(stateDir) {
|
|
|
242
330
|
openssl(["ecparam", "-genkey", "-name", "prime256v1", "-noout", "-out", serverKeyPath]);
|
|
243
331
|
openssl(["req", "-new", "-key", serverKeyPath, "-out", csrPath, "-subj", "/CN=localhost"]);
|
|
244
332
|
const sans = ["DNS:localhost", "DNS:*.localhost", "DNS:*.local"];
|
|
245
|
-
|
|
333
|
+
fs2.writeFileSync(
|
|
246
334
|
extPath,
|
|
247
335
|
[
|
|
248
336
|
"authorityKeyIdentifier=keyid,issuer",
|
|
@@ -254,9 +342,9 @@ function generateServerCert(stateDir) {
|
|
|
254
342
|
);
|
|
255
343
|
const srlPath = path.join(stateDir, "ca.srl");
|
|
256
344
|
if (!fileExists(srlPath)) {
|
|
257
|
-
|
|
345
|
+
fs2.writeFileSync(
|
|
258
346
|
srlPath,
|
|
259
|
-
|
|
347
|
+
crypto2.randomUUID().replace(/-/g, "").slice(0, 16).toUpperCase() + "\n"
|
|
260
348
|
);
|
|
261
349
|
}
|
|
262
350
|
openssl([
|
|
@@ -280,12 +368,12 @@ function generateServerCert(stateDir) {
|
|
|
280
368
|
]);
|
|
281
369
|
for (const tmp of [csrPath, extPath]) {
|
|
282
370
|
try {
|
|
283
|
-
|
|
371
|
+
fs2.unlinkSync(tmp);
|
|
284
372
|
} catch {
|
|
285
373
|
}
|
|
286
374
|
}
|
|
287
|
-
|
|
288
|
-
|
|
375
|
+
fs2.chmodSync(serverKeyPath, 384);
|
|
376
|
+
fs2.chmodSync(serverCertPath, 420);
|
|
289
377
|
fixOwnership(serverKeyPath, serverCertPath);
|
|
290
378
|
return { certPath: serverCertPath, keyPath: serverKeyPath };
|
|
291
379
|
}
|
|
@@ -316,36 +404,30 @@ function isCATrusted(stateDir) {
|
|
|
316
404
|
const marker = readTrustMarker(stateDir);
|
|
317
405
|
if (marker) {
|
|
318
406
|
const fp = caFingerprint(stateDir);
|
|
319
|
-
|
|
407
|
+
const expected = fp && isWSL() ? `wsl:${fp}` : fp;
|
|
408
|
+
if (expected && marker === expected) return true;
|
|
320
409
|
}
|
|
321
410
|
if (process.platform === "darwin") {
|
|
322
411
|
return isCATrustedMacOS(caCertPath);
|
|
323
412
|
} else if (process.platform === "linux") {
|
|
324
|
-
|
|
413
|
+
if (!isCATrustedLinux(stateDir)) return false;
|
|
414
|
+
if (!isWSL()) return true;
|
|
415
|
+
try {
|
|
416
|
+
return isWindowsCATrusted(caCertPath, wslWindowsCAStoreOptions());
|
|
417
|
+
} catch {
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
325
420
|
} else if (process.platform === "win32") {
|
|
326
|
-
return
|
|
421
|
+
return isWindowsCATrusted(caCertPath);
|
|
327
422
|
}
|
|
328
423
|
return false;
|
|
329
424
|
}
|
|
330
|
-
function isCATrustedWindows(caCertPath) {
|
|
331
|
-
try {
|
|
332
|
-
const fingerprint = openssl(["x509", "-in", caCertPath, "-noout", "-fingerprint", "-sha1"]).trim().replace(/^.*=/, "").replace(/:/g, "").toLowerCase();
|
|
333
|
-
const result = execFileSync("certutil", ["-store", "-user", "Root"], {
|
|
334
|
-
encoding: "utf-8",
|
|
335
|
-
timeout: 1e4,
|
|
336
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
337
|
-
});
|
|
338
|
-
return result.replace(/\s/g, "").toLowerCase().includes(fingerprint);
|
|
339
|
-
} catch {
|
|
340
|
-
return false;
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
425
|
function isCATrustedMacOS(caCertPath) {
|
|
344
426
|
try {
|
|
345
427
|
const isRoot = (process.getuid?.() ?? -1) === 0;
|
|
346
428
|
const sudoUser = process.env.SUDO_USER;
|
|
347
429
|
if (isRoot && sudoUser) {
|
|
348
|
-
|
|
430
|
+
execFileSync2(
|
|
349
431
|
"sudo",
|
|
350
432
|
["-u", sudoUser, "security", "verify-cert", "-c", caCertPath, "-L", "-p", "ssl"],
|
|
351
433
|
{
|
|
@@ -354,7 +436,7 @@ function isCATrustedMacOS(caCertPath) {
|
|
|
354
436
|
}
|
|
355
437
|
);
|
|
356
438
|
} else {
|
|
357
|
-
|
|
439
|
+
execFileSync2("security", ["verify-cert", "-c", caCertPath, "-L", "-p", "ssl"], {
|
|
358
440
|
stdio: "pipe",
|
|
359
441
|
timeout: MACOS_SECURITY_TIMEOUT_MS
|
|
360
442
|
});
|
|
@@ -366,7 +448,7 @@ function isCATrustedMacOS(caCertPath) {
|
|
|
366
448
|
}
|
|
367
449
|
function loginKeychainPath() {
|
|
368
450
|
try {
|
|
369
|
-
const result =
|
|
451
|
+
const result = execFileSync2("security", ["default-keychain"], {
|
|
370
452
|
encoding: "utf-8",
|
|
371
453
|
timeout: MACOS_SECURITY_TIMEOUT_MS
|
|
372
454
|
}).trim();
|
|
@@ -397,7 +479,7 @@ var LINUX_CA_TRUST_CONFIGS = {
|
|
|
397
479
|
};
|
|
398
480
|
function detectLinuxDistro() {
|
|
399
481
|
try {
|
|
400
|
-
const osRelease =
|
|
482
|
+
const osRelease = fs2.readFileSync("/etc/os-release", "utf-8").toLowerCase();
|
|
401
483
|
if (osRelease.includes("arch")) return "arch";
|
|
402
484
|
if (osRelease.includes("fedora") || osRelease.includes("rhel") || osRelease.includes("centos"))
|
|
403
485
|
return "fedora";
|
|
@@ -407,8 +489,8 @@ function detectLinuxDistro() {
|
|
|
407
489
|
}
|
|
408
490
|
for (const [distro, config] of Object.entries(LINUX_CA_TRUST_CONFIGS)) {
|
|
409
491
|
try {
|
|
410
|
-
|
|
411
|
-
if (
|
|
492
|
+
execFileSync2("which", [config.updateCommand], { stdio: "pipe", timeout: 5e3 });
|
|
493
|
+
if (fs2.existsSync(path.dirname(config.certDir))) return distro;
|
|
412
494
|
} catch {
|
|
413
495
|
}
|
|
414
496
|
}
|
|
@@ -418,13 +500,12 @@ function getLinuxCATrustConfig() {
|
|
|
418
500
|
const distro = detectLinuxDistro();
|
|
419
501
|
return LINUX_CA_TRUST_CONFIGS[distro ?? "debian"];
|
|
420
502
|
}
|
|
421
|
-
function isCATrustedLinux(stateDir) {
|
|
422
|
-
const config = getLinuxCATrustConfig();
|
|
503
|
+
function isCATrustedLinux(stateDir, config = getLinuxCATrustConfig()) {
|
|
423
504
|
const systemCertPath = path.join(config.certDir, "portless-ca.crt");
|
|
424
505
|
if (!fileExists(systemCertPath)) return false;
|
|
425
506
|
try {
|
|
426
|
-
const ours =
|
|
427
|
-
const installed =
|
|
507
|
+
const ours = fs2.readFileSync(path.join(stateDir, CA_CERT_FILE), "utf-8").trim();
|
|
508
|
+
const installed = fs2.readFileSync(systemCertPath, "utf-8").trim();
|
|
428
509
|
return ours === installed;
|
|
429
510
|
} catch {
|
|
430
511
|
return false;
|
|
@@ -439,8 +520,8 @@ async function generateHostCertAsync(stateDir, hostname) {
|
|
|
439
520
|
const caKeyPath = path.join(stateDir, CA_KEY_FILE);
|
|
440
521
|
const caCertPath = path.join(stateDir, CA_CERT_FILE);
|
|
441
522
|
const hostDir = path.join(stateDir, HOST_CERTS_DIR);
|
|
442
|
-
if (!
|
|
443
|
-
await
|
|
523
|
+
if (!fs2.existsSync(hostDir)) {
|
|
524
|
+
await fs2.promises.mkdir(hostDir, { recursive: true, mode: 493 });
|
|
444
525
|
fixOwnership(hostDir);
|
|
445
526
|
}
|
|
446
527
|
const safeName = sanitizeHostForFilename(hostname);
|
|
@@ -456,7 +537,7 @@ async function generateHostCertAsync(stateDir, hostname) {
|
|
|
456
537
|
if (parts.length >= 2) {
|
|
457
538
|
sans.push(`DNS:*.${parts.slice(1).join(".")}`);
|
|
458
539
|
}
|
|
459
|
-
await
|
|
540
|
+
await fs2.promises.writeFile(
|
|
460
541
|
extPath,
|
|
461
542
|
[
|
|
462
543
|
"authorityKeyIdentifier=keyid,issuer",
|
|
@@ -467,10 +548,10 @@ async function generateHostCertAsync(stateDir, hostname) {
|
|
|
467
548
|
].join("\n") + "\n"
|
|
468
549
|
);
|
|
469
550
|
const srlPath = path.join(stateDir, "ca.srl");
|
|
470
|
-
if (!
|
|
471
|
-
await
|
|
551
|
+
if (!fs2.existsSync(srlPath)) {
|
|
552
|
+
await fs2.promises.writeFile(
|
|
472
553
|
srlPath,
|
|
473
|
-
|
|
554
|
+
crypto2.randomUUID().replace(/-/g, "").slice(0, 16).toUpperCase() + "\n"
|
|
474
555
|
);
|
|
475
556
|
}
|
|
476
557
|
await opensslAsync([
|
|
@@ -494,12 +575,12 @@ async function generateHostCertAsync(stateDir, hostname) {
|
|
|
494
575
|
]);
|
|
495
576
|
for (const tmp of [csrPath, extPath]) {
|
|
496
577
|
try {
|
|
497
|
-
await
|
|
578
|
+
await fs2.promises.unlink(tmp);
|
|
498
579
|
} catch {
|
|
499
580
|
}
|
|
500
581
|
}
|
|
501
|
-
await
|
|
502
|
-
await
|
|
582
|
+
await fs2.promises.chmod(keyPath, 384);
|
|
583
|
+
await fs2.promises.chmod(certPath, 420);
|
|
503
584
|
fixOwnership(keyPath, certPath);
|
|
504
585
|
return { certPath, keyPath };
|
|
505
586
|
}
|
|
@@ -526,10 +607,10 @@ function createSNICallback(stateDir, defaultCert, defaultKey, tlds = "localhost"
|
|
|
526
607
|
const keyPath = path.join(hostDir, `${safeName}-key.pem`);
|
|
527
608
|
if (fileExists(certPath) && fileExists(keyPath) && isCertValid(certPath) && isCertSignatureStrong(certPath)) {
|
|
528
609
|
try {
|
|
529
|
-
const hostCert =
|
|
610
|
+
const hostCert = fs2.readFileSync(certPath);
|
|
530
611
|
const ctx = tls.createSecureContext({
|
|
531
612
|
cert: caCert ? Buffer.concat([hostCert, caCert]) : hostCert,
|
|
532
|
-
key:
|
|
613
|
+
key: fs2.readFileSync(keyPath)
|
|
533
614
|
});
|
|
534
615
|
cache.set(servername, ctx);
|
|
535
616
|
cb(null, ctx);
|
|
@@ -543,8 +624,8 @@ function createSNICallback(stateDir, defaultCert, defaultKey, tlds = "localhost"
|
|
|
543
624
|
}
|
|
544
625
|
const promise = generateHostCertAsync(stateDir, servername).then(async (generated) => {
|
|
545
626
|
const [hostCert, key] = await Promise.all([
|
|
546
|
-
|
|
547
|
-
|
|
627
|
+
fs2.promises.readFile(generated.certPath),
|
|
628
|
+
fs2.promises.readFile(generated.keyPath)
|
|
548
629
|
]);
|
|
549
630
|
return tls.createSecureContext({
|
|
550
631
|
cert: caCert ? Buffer.concat([hostCert, caCert]) : hostCert,
|
|
@@ -574,7 +655,7 @@ function trustCA(stateDir) {
|
|
|
574
655
|
if (process.platform === "darwin") {
|
|
575
656
|
const isRoot = (process.getuid?.() ?? -1) === 0;
|
|
576
657
|
if (isRoot) {
|
|
577
|
-
|
|
658
|
+
execFileSync2(
|
|
578
659
|
"security",
|
|
579
660
|
[
|
|
580
661
|
"add-trusted-cert",
|
|
@@ -589,7 +670,7 @@ function trustCA(stateDir) {
|
|
|
589
670
|
);
|
|
590
671
|
} else {
|
|
591
672
|
const keychain = loginKeychainPath();
|
|
592
|
-
|
|
673
|
+
execFileSync2(
|
|
593
674
|
"security",
|
|
594
675
|
["add-trusted-cert", "-r", "trustRoot", "-k", keychain, caCertPath],
|
|
595
676
|
{ stdio: "pipe", timeout: MACOS_SECURITY_AUTH_TIMEOUT_MS }
|
|
@@ -599,19 +680,19 @@ function trustCA(stateDir) {
|
|
|
599
680
|
return { trusted: true };
|
|
600
681
|
} else if (process.platform === "linux") {
|
|
601
682
|
const config = getLinuxCATrustConfig();
|
|
602
|
-
if (!
|
|
603
|
-
|
|
683
|
+
if (!fs2.existsSync(config.certDir)) {
|
|
684
|
+
fs2.mkdirSync(config.certDir, { recursive: true });
|
|
604
685
|
}
|
|
605
686
|
const dest = path.join(config.certDir, "portless-ca.crt");
|
|
606
|
-
|
|
607
|
-
|
|
687
|
+
fs2.copyFileSync(caCertPath, dest);
|
|
688
|
+
execFileSync2(config.updateCommand, [], { stdio: "pipe", timeout: 3e4 });
|
|
689
|
+
if (isWSL()) {
|
|
690
|
+
trustWindowsCA(caCertPath, wslWindowsCAStoreOptions());
|
|
691
|
+
}
|
|
608
692
|
writeTrustMarker(stateDir);
|
|
609
693
|
return { trusted: true };
|
|
610
694
|
} else if (process.platform === "win32") {
|
|
611
|
-
|
|
612
|
-
stdio: "pipe",
|
|
613
|
-
timeout: 3e4
|
|
614
|
-
});
|
|
695
|
+
trustWindowsCA(caCertPath);
|
|
615
696
|
writeTrustMarker(stateDir);
|
|
616
697
|
return { trusted: true };
|
|
617
698
|
}
|
|
@@ -637,18 +718,15 @@ function untrustCA(stateDir) {
|
|
|
637
718
|
clearTrustMarker(stateDir);
|
|
638
719
|
return { removed: true };
|
|
639
720
|
}
|
|
640
|
-
|
|
641
|
-
clearTrustMarker(stateDir);
|
|
642
|
-
return { removed: true };
|
|
643
|
-
}
|
|
721
|
+
const runningInWSL = isWSL();
|
|
644
722
|
try {
|
|
645
723
|
let result;
|
|
646
724
|
if (process.platform === "darwin") {
|
|
647
725
|
result = untrustCAMacOS(caCertPath);
|
|
648
726
|
} else if (process.platform === "linux") {
|
|
649
|
-
result = untrustCALinux(stateDir);
|
|
727
|
+
result = runningInWSL ? untrustCAWSL(stateDir, caCertPath) : untrustCALinux(stateDir);
|
|
650
728
|
} else if (process.platform === "win32") {
|
|
651
|
-
result =
|
|
729
|
+
result = untrustWindowsCA(caCertPath);
|
|
652
730
|
} else {
|
|
653
731
|
result = { removed: false, error: `Unsupported platform: ${process.platform}` };
|
|
654
732
|
}
|
|
@@ -663,7 +741,7 @@ function untrustCAMacOS(caCertPath) {
|
|
|
663
741
|
const errors = [];
|
|
664
742
|
const tryExec = (args) => {
|
|
665
743
|
try {
|
|
666
|
-
|
|
744
|
+
execFileSync2("security", args, { stdio: "pipe", timeout: MACOS_SECURITY_ROOT_TIMEOUT_MS });
|
|
667
745
|
return true;
|
|
668
746
|
} catch (err) {
|
|
669
747
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -685,13 +763,13 @@ function isCATrustedMacOSAfterAttempt(caCertPath) {
|
|
|
685
763
|
const isRoot = (process.getuid?.() ?? -1) === 0;
|
|
686
764
|
const sudoUser = process.env.SUDO_USER;
|
|
687
765
|
if (isRoot && sudoUser) {
|
|
688
|
-
|
|
766
|
+
execFileSync2(
|
|
689
767
|
"sudo",
|
|
690
768
|
["-u", sudoUser, "security", "verify-cert", "-c", caCertPath, "-L", "-p", "ssl"],
|
|
691
769
|
{ stdio: "pipe", timeout: MACOS_SECURITY_TIMEOUT_MS }
|
|
692
770
|
);
|
|
693
771
|
} else {
|
|
694
|
-
|
|
772
|
+
execFileSync2("security", ["verify-cert", "-c", caCertPath, "-L", "-p", "ssl"], {
|
|
695
773
|
stdio: "pipe",
|
|
696
774
|
timeout: MACOS_SECURITY_TIMEOUT_MS
|
|
697
775
|
});
|
|
@@ -701,63 +779,67 @@ function isCATrustedMacOSAfterAttempt(caCertPath) {
|
|
|
701
779
|
return false;
|
|
702
780
|
}
|
|
703
781
|
}
|
|
704
|
-
function untrustCALinux(stateDir) {
|
|
782
|
+
function untrustCALinux(stateDir, options = {}) {
|
|
705
783
|
const errors = [];
|
|
706
|
-
|
|
707
|
-
|
|
784
|
+
const pendingRefreshPath = path.join(stateDir, CA_TRUST_REFRESH_PENDING);
|
|
785
|
+
let refreshNeeded = fileExists(pendingRefreshPath);
|
|
786
|
+
const configs = options.configs ?? Object.values(LINUX_CA_TRUST_CONFIGS);
|
|
787
|
+
const activeConfig = options.activeConfig ?? getLinuxCATrustConfig();
|
|
788
|
+
const runUpdate = options.runUpdate ?? ((command) => {
|
|
789
|
+
execFileSync2(command, [], { stdio: "pipe", timeout: 3e4 });
|
|
790
|
+
});
|
|
791
|
+
for (const config of configs) {
|
|
708
792
|
const dest = path.join(config.certDir, "portless-ca.crt");
|
|
709
793
|
try {
|
|
710
794
|
if (fileExists(dest)) {
|
|
711
|
-
const ours =
|
|
712
|
-
const installed =
|
|
795
|
+
const ours = fs2.readFileSync(path.join(stateDir, CA_CERT_FILE), "utf-8").trim();
|
|
796
|
+
const installed = fs2.readFileSync(dest, "utf-8").trim();
|
|
713
797
|
if (ours === installed) {
|
|
714
|
-
|
|
715
|
-
|
|
798
|
+
if (!refreshNeeded) {
|
|
799
|
+
fs2.writeFileSync(pendingRefreshPath, "1\n");
|
|
800
|
+
fixOwnership(pendingRefreshPath);
|
|
801
|
+
refreshNeeded = true;
|
|
802
|
+
}
|
|
803
|
+
fs2.unlinkSync(dest);
|
|
716
804
|
}
|
|
717
805
|
}
|
|
718
806
|
} catch (err) {
|
|
719
807
|
errors.push(err instanceof Error ? err.message : String(err));
|
|
720
808
|
}
|
|
721
809
|
}
|
|
722
|
-
if (
|
|
810
|
+
if (refreshNeeded) {
|
|
723
811
|
try {
|
|
724
|
-
|
|
725
|
-
execFileSync(config.updateCommand, [], { stdio: "pipe", timeout: 3e4 });
|
|
812
|
+
runUpdate(activeConfig.updateCommand);
|
|
726
813
|
} catch (err) {
|
|
727
814
|
errors.push(err instanceof Error ? err.message : String(err));
|
|
728
815
|
}
|
|
729
816
|
}
|
|
730
|
-
if (
|
|
817
|
+
if (errors.length > 0 || isCATrustedLinux(stateDir, activeConfig)) {
|
|
731
818
|
return {
|
|
732
819
|
removed: false,
|
|
733
820
|
error: errors.join("; ") || "CA still trusted (remove portless-ca.crt and run the distro CA update command, often with sudo)"
|
|
734
821
|
};
|
|
735
822
|
}
|
|
823
|
+
clearTrustRefreshPending(stateDir);
|
|
736
824
|
return { removed: true };
|
|
737
825
|
}
|
|
738
|
-
function
|
|
826
|
+
function untrustCAWSL(stateDir, caCertPath) {
|
|
827
|
+
const linuxResult = untrustCALinux(stateDir);
|
|
828
|
+
let windowsResult;
|
|
739
829
|
try {
|
|
740
|
-
|
|
741
|
-
const storeListing = execFileSync("certutil", ["-store", "-user", "Root"], {
|
|
742
|
-
encoding: "utf-8",
|
|
743
|
-
timeout: 1e4,
|
|
744
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
745
|
-
});
|
|
746
|
-
const normalized = storeListing.replace(/\s/g, "").toLowerCase();
|
|
747
|
-
if (!normalized.includes(fingerprint)) {
|
|
748
|
-
return { removed: true };
|
|
749
|
-
}
|
|
750
|
-
execFileSync("certutil", ["-delstore", "-user", "Root", "portless Local CA"], {
|
|
751
|
-
stdio: "pipe",
|
|
752
|
-
timeout: 3e4
|
|
753
|
-
});
|
|
754
|
-
if (isCATrustedWindows(caCertPath)) {
|
|
755
|
-
return { removed: false, error: "certutil could not remove the portless CA from Root" };
|
|
756
|
-
}
|
|
757
|
-
return { removed: true };
|
|
830
|
+
windowsResult = untrustWindowsCA(caCertPath, wslWindowsCAStoreOptions());
|
|
758
831
|
} catch (err) {
|
|
759
|
-
|
|
832
|
+
windowsResult = {
|
|
833
|
+
removed: false,
|
|
834
|
+
error: err instanceof Error ? err.message : String(err)
|
|
835
|
+
};
|
|
760
836
|
}
|
|
837
|
+
if (linuxResult.removed && windowsResult.removed) return { removed: true };
|
|
838
|
+
const errors = [linuxResult.error, windowsResult.error].filter(Boolean);
|
|
839
|
+
return {
|
|
840
|
+
removed: false,
|
|
841
|
+
error: errors.join("; ") || "Could not remove the portless CA from every WSL trust store"
|
|
842
|
+
};
|
|
761
843
|
}
|
|
762
844
|
|
|
763
845
|
// src/tailscale.ts
|
|
@@ -766,7 +848,7 @@ var TAILSCALE_BINARY = "tailscale";
|
|
|
766
848
|
var TAILSCALE_COMMAND_TIMEOUT_MS = 3e4;
|
|
767
849
|
var PREFERRED_SERVE_PORTS = [443, 8443, 8444, 8445, 8446, 8447, 8448, 8449, 8450];
|
|
768
850
|
var FUNNEL_PORTS = [443, 8443, 1e4];
|
|
769
|
-
function
|
|
851
|
+
function defaultRunner2(args) {
|
|
770
852
|
const result = spawnSync(TAILSCALE_BINARY, args, {
|
|
771
853
|
encoding: "utf-8",
|
|
772
854
|
killSignal: "SIGKILL",
|
|
@@ -858,7 +940,7 @@ function throwFunnelNotEnabled(status) {
|
|
|
858
940
|
);
|
|
859
941
|
}
|
|
860
942
|
function ensureTailscaleReady(options = {}) {
|
|
861
|
-
const runner = options.runner ??
|
|
943
|
+
const runner = options.runner ?? defaultRunner2;
|
|
862
944
|
runOrThrow(["version"], "check tailscale version", runner);
|
|
863
945
|
const statusResult = runOrThrow(["status", "--json"], "read tailscale status", runner);
|
|
864
946
|
const status = parseStatusJson(statusResult.stdout);
|
|
@@ -874,7 +956,7 @@ function ensureTailscaleReady(options = {}) {
|
|
|
874
956
|
baseUrl: `https://${dnsName}`
|
|
875
957
|
};
|
|
876
958
|
}
|
|
877
|
-
function getUsedServePorts(runner =
|
|
959
|
+
function getUsedServePorts(runner = defaultRunner2) {
|
|
878
960
|
const result = runner(["serve", "status", "--json"]);
|
|
879
961
|
if (result.error || result.status !== 0) {
|
|
880
962
|
return /* @__PURE__ */ new Set();
|
|
@@ -970,7 +1052,7 @@ function register(mode, localPort, httpsPort, runner) {
|
|
|
970
1052
|
}
|
|
971
1053
|
}
|
|
972
1054
|
function unregister(mode, httpsPort, options) {
|
|
973
|
-
const runner = options?.runner ??
|
|
1055
|
+
const runner = options?.runner ?? defaultRunner2;
|
|
974
1056
|
const result = runner([mode, "--yes", `--https=${httpsPort}`, "off"]);
|
|
975
1057
|
if (result.error) {
|
|
976
1058
|
const errno = result.error;
|
|
@@ -989,10 +1071,10 @@ ${result.stdout}`.toLowerCase();
|
|
|
989
1071
|
}
|
|
990
1072
|
}
|
|
991
1073
|
function registerServe(localPort, httpsPort, options) {
|
|
992
|
-
register("serve", localPort, httpsPort, options?.runner ??
|
|
1074
|
+
register("serve", localPort, httpsPort, options?.runner ?? defaultRunner2);
|
|
993
1075
|
}
|
|
994
1076
|
function registerFunnel(localPort, httpsPort, options) {
|
|
995
|
-
register("funnel", localPort, httpsPort, options?.runner ??
|
|
1077
|
+
register("funnel", localPort, httpsPort, options?.runner ?? defaultRunner2);
|
|
996
1078
|
}
|
|
997
1079
|
function unregisterTailscale(route) {
|
|
998
1080
|
if (!route.tailscaleHttpsPort) return;
|
|
@@ -1017,7 +1099,7 @@ function defaultSpawner(args) {
|
|
|
1017
1099
|
windowsHide: true
|
|
1018
1100
|
});
|
|
1019
1101
|
}
|
|
1020
|
-
function
|
|
1102
|
+
function defaultRunner3(args) {
|
|
1021
1103
|
const result = spawnSync2(NGROK_BINARY, args, {
|
|
1022
1104
|
encoding: "utf-8",
|
|
1023
1105
|
killSignal: "SIGKILL",
|
|
@@ -1054,7 +1136,7 @@ function formatOutputError(output) {
|
|
|
1054
1136
|
`Failed to start ngrok tunnel: ${details || "ngrok exited before printing a public URL"}`
|
|
1055
1137
|
);
|
|
1056
1138
|
}
|
|
1057
|
-
function ensureNgrokAvailable(runner =
|
|
1139
|
+
function ensureNgrokAvailable(runner = defaultRunner3) {
|
|
1058
1140
|
const result = runner(["version"]);
|
|
1059
1141
|
if (result.error) {
|
|
1060
1142
|
throw formatSpawnError(result.error);
|
|
@@ -1179,8 +1261,8 @@ function stopNgrok(route) {
|
|
|
1179
1261
|
|
|
1180
1262
|
// src/auto.ts
|
|
1181
1263
|
import { createHash as createHash2 } from "crypto";
|
|
1182
|
-
import { execFileSync as
|
|
1183
|
-
import * as
|
|
1264
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
1265
|
+
import * as fs3 from "fs";
|
|
1184
1266
|
import * as path2 from "path";
|
|
1185
1267
|
var MAX_DNS_LABEL_LENGTH = 63;
|
|
1186
1268
|
function truncateLabel(label) {
|
|
@@ -1220,7 +1302,7 @@ function findPackageJsonName(startDir) {
|
|
|
1220
1302
|
for (; ; ) {
|
|
1221
1303
|
const pkgPath = path2.join(dir, "package.json");
|
|
1222
1304
|
try {
|
|
1223
|
-
const raw =
|
|
1305
|
+
const raw = fs3.readFileSync(pkgPath, "utf-8");
|
|
1224
1306
|
const pkg = JSON.parse(raw);
|
|
1225
1307
|
if (typeof pkg.name === "string" && pkg.name) {
|
|
1226
1308
|
return pkg.name.replace(/^@[^/]+\//, "");
|
|
@@ -1235,7 +1317,7 @@ function findPackageJsonName(startDir) {
|
|
|
1235
1317
|
}
|
|
1236
1318
|
function findGitRoot(startDir) {
|
|
1237
1319
|
try {
|
|
1238
|
-
const toplevel =
|
|
1320
|
+
const toplevel = execFileSync3("git", ["rev-parse", "--show-toplevel"], {
|
|
1239
1321
|
cwd: startDir,
|
|
1240
1322
|
encoding: "utf-8",
|
|
1241
1323
|
timeout: 5e3,
|
|
@@ -1248,7 +1330,7 @@ function findGitRoot(startDir) {
|
|
|
1248
1330
|
for (; ; ) {
|
|
1249
1331
|
const gitPath = path2.join(dir, ".git");
|
|
1250
1332
|
try {
|
|
1251
|
-
const stat =
|
|
1333
|
+
const stat = fs3.statSync(gitPath);
|
|
1252
1334
|
if (stat.isDirectory()) return dir;
|
|
1253
1335
|
if (stat.isFile()) return dir;
|
|
1254
1336
|
} catch {
|
|
@@ -1276,7 +1358,7 @@ function detectWorktreePrefix(cwd = process.cwd()) {
|
|
|
1276
1358
|
}
|
|
1277
1359
|
function detectWorktreeViaCli(cwd) {
|
|
1278
1360
|
try {
|
|
1279
|
-
const listOutput =
|
|
1361
|
+
const listOutput = execFileSync3("git", ["worktree", "list", "--porcelain"], {
|
|
1280
1362
|
cwd,
|
|
1281
1363
|
encoding: "utf-8",
|
|
1282
1364
|
timeout: 5e3,
|
|
@@ -1286,7 +1368,7 @@ function detectWorktreeViaCli(cwd) {
|
|
|
1286
1368
|
if (worktreeCount <= 1) return null;
|
|
1287
1369
|
const gitDir = path2.resolve(
|
|
1288
1370
|
cwd,
|
|
1289
|
-
|
|
1371
|
+
execFileSync3("git", ["rev-parse", "--git-dir"], {
|
|
1290
1372
|
cwd,
|
|
1291
1373
|
encoding: "utf-8",
|
|
1292
1374
|
timeout: 5e3,
|
|
@@ -1295,7 +1377,7 @@ function detectWorktreeViaCli(cwd) {
|
|
|
1295
1377
|
);
|
|
1296
1378
|
const gitCommonDir = path2.resolve(
|
|
1297
1379
|
cwd,
|
|
1298
|
-
|
|
1380
|
+
execFileSync3("git", ["rev-parse", "--git-common-dir"], {
|
|
1299
1381
|
cwd,
|
|
1300
1382
|
encoding: "utf-8",
|
|
1301
1383
|
timeout: 5e3,
|
|
@@ -1303,7 +1385,7 @@ function detectWorktreeViaCli(cwd) {
|
|
|
1303
1385
|
}).trim()
|
|
1304
1386
|
);
|
|
1305
1387
|
if (gitDir === gitCommonDir) return null;
|
|
1306
|
-
const branch =
|
|
1388
|
+
const branch = execFileSync3("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
1307
1389
|
cwd,
|
|
1308
1390
|
encoding: "utf-8",
|
|
1309
1391
|
timeout: 5e3,
|
|
@@ -1321,12 +1403,12 @@ function detectWorktreeViaFilesystem(startDir) {
|
|
|
1321
1403
|
for (; ; ) {
|
|
1322
1404
|
const gitPath = path2.join(dir, ".git");
|
|
1323
1405
|
try {
|
|
1324
|
-
const stat =
|
|
1406
|
+
const stat = fs3.statSync(gitPath);
|
|
1325
1407
|
if (stat.isDirectory()) {
|
|
1326
1408
|
return null;
|
|
1327
1409
|
}
|
|
1328
1410
|
if (stat.isFile()) {
|
|
1329
|
-
const content =
|
|
1411
|
+
const content = fs3.readFileSync(gitPath, "utf-8").trim();
|
|
1330
1412
|
const match = content.match(/^gitdir:\s*(.+)$/);
|
|
1331
1413
|
if (!match) return null;
|
|
1332
1414
|
const gitdir = match[1];
|
|
@@ -1346,7 +1428,7 @@ function detectWorktreeViaFilesystem(startDir) {
|
|
|
1346
1428
|
}
|
|
1347
1429
|
function readBranchFromHead(gitdir) {
|
|
1348
1430
|
try {
|
|
1349
|
-
const head =
|
|
1431
|
+
const head = fs3.readFileSync(path2.join(gitdir, "HEAD"), "utf-8").trim();
|
|
1350
1432
|
const refMatch = head.match(/^ref: refs\/heads\/(.+)$/);
|
|
1351
1433
|
return refMatch ? refMatch[1] : null;
|
|
1352
1434
|
} catch {
|
|
@@ -1355,11 +1437,11 @@ function readBranchFromHead(gitdir) {
|
|
|
1355
1437
|
}
|
|
1356
1438
|
|
|
1357
1439
|
// src/cli-utils.ts
|
|
1358
|
-
import * as
|
|
1440
|
+
import * as fs4 from "fs";
|
|
1359
1441
|
import * as http from "http";
|
|
1360
1442
|
import * as https from "https";
|
|
1361
1443
|
import * as net from "net";
|
|
1362
|
-
import * as
|
|
1444
|
+
import * as os2 from "os";
|
|
1363
1445
|
import * as path3 from "path";
|
|
1364
1446
|
import * as readline from "readline";
|
|
1365
1447
|
import { execSync, spawn as spawn2 } from "child_process";
|
|
@@ -1368,8 +1450,12 @@ var FALLBACK_PROXY_PORT = 1355;
|
|
|
1368
1450
|
var PRIVILEGED_PORT_THRESHOLD = 1024;
|
|
1369
1451
|
var INTERNAL_LAN_IP_ENV = "PORTLESS_INTERNAL_LAN_IP";
|
|
1370
1452
|
var INTERNAL_LAN_IP_FLAG = "--lan-ip-auto";
|
|
1371
|
-
var
|
|
1372
|
-
var
|
|
1453
|
+
var IPV4_LOOPBACK_PROXY_HOST = "127.0.0.1";
|
|
1454
|
+
var IPV6_LOOPBACK_PROXY_HOST = "::1";
|
|
1455
|
+
var IPV4_LAN_PROXY_HOST = "0.0.0.0";
|
|
1456
|
+
var IPV6_LAN_PROXY_HOST = "::";
|
|
1457
|
+
var LEGACY_SYSTEM_STATE_DIR = isWindows ? path3.join(os2.tmpdir(), "portless") : "/tmp/portless";
|
|
1458
|
+
var USER_STATE_DIR = path3.join(resolveUserHome(), ".portless");
|
|
1373
1459
|
var MIN_APP_PORT = 4e3;
|
|
1374
1460
|
var MAX_APP_PORT = 4999;
|
|
1375
1461
|
var RANDOM_PORT_ATTEMPTS = 50;
|
|
@@ -1470,6 +1556,12 @@ var SIGNAL_CODES = {
|
|
|
1470
1556
|
SIGKILL: 9,
|
|
1471
1557
|
SIGTERM: 15
|
|
1472
1558
|
};
|
|
1559
|
+
function getProxyBindTargets(lanMode) {
|
|
1560
|
+
return lanMode ? [{ host: IPV4_LAN_PROXY_HOST }, { host: IPV6_LAN_PROXY_HOST, ipv6Only: true }] : [{ host: IPV4_LOOPBACK_PROXY_HOST }, { host: IPV6_LOOPBACK_PROXY_HOST, ipv6Only: true }];
|
|
1561
|
+
}
|
|
1562
|
+
function listenOnProxyInterface(server, port, target, listener) {
|
|
1563
|
+
server.listen({ port, host: target.host, ipv6Only: target.ipv6Only }, listener);
|
|
1564
|
+
}
|
|
1473
1565
|
function killTree(child, signal = "SIGTERM") {
|
|
1474
1566
|
if (!child.pid) {
|
|
1475
1567
|
child.kill(signal);
|
|
@@ -1504,7 +1596,7 @@ function resolveStateDir(_port) {
|
|
|
1504
1596
|
}
|
|
1505
1597
|
function readPortFromDir(dir) {
|
|
1506
1598
|
try {
|
|
1507
|
-
const raw =
|
|
1599
|
+
const raw = fs4.readFileSync(path3.join(dir, "proxy.port"), "utf-8").trim();
|
|
1508
1600
|
const port = parseInt(raw, 10);
|
|
1509
1601
|
return isNaN(port) ? null : port;
|
|
1510
1602
|
} catch {
|
|
@@ -1515,7 +1607,7 @@ var TLS_MARKER_FILE = "proxy.tls";
|
|
|
1515
1607
|
var CUSTOM_CERT_MARKER_FILE = "proxy.custom-cert";
|
|
1516
1608
|
function readTlsMarker(dir) {
|
|
1517
1609
|
try {
|
|
1518
|
-
return
|
|
1610
|
+
return fs4.existsSync(path3.join(dir, TLS_MARKER_FILE));
|
|
1519
1611
|
} catch {
|
|
1520
1612
|
return false;
|
|
1521
1613
|
}
|
|
@@ -1523,17 +1615,17 @@ function readTlsMarker(dir) {
|
|
|
1523
1615
|
function writeTlsMarker(dir, enabled2) {
|
|
1524
1616
|
const markerPath = path3.join(dir, TLS_MARKER_FILE);
|
|
1525
1617
|
if (enabled2) {
|
|
1526
|
-
|
|
1618
|
+
fs4.writeFileSync(markerPath, "1", { mode: 420 });
|
|
1527
1619
|
} else {
|
|
1528
1620
|
try {
|
|
1529
|
-
|
|
1621
|
+
fs4.unlinkSync(markerPath);
|
|
1530
1622
|
} catch {
|
|
1531
1623
|
}
|
|
1532
1624
|
}
|
|
1533
1625
|
}
|
|
1534
1626
|
function readCustomCertMarker(dir) {
|
|
1535
1627
|
try {
|
|
1536
|
-
return
|
|
1628
|
+
return fs4.existsSync(path3.join(dir, CUSTOM_CERT_MARKER_FILE));
|
|
1537
1629
|
} catch {
|
|
1538
1630
|
return false;
|
|
1539
1631
|
}
|
|
@@ -1541,10 +1633,10 @@ function readCustomCertMarker(dir) {
|
|
|
1541
1633
|
function writeCustomCertMarker(dir, enabled2) {
|
|
1542
1634
|
const markerPath = path3.join(dir, CUSTOM_CERT_MARKER_FILE);
|
|
1543
1635
|
if (enabled2) {
|
|
1544
|
-
|
|
1636
|
+
fs4.writeFileSync(markerPath, "1", { mode: 420 });
|
|
1545
1637
|
} else {
|
|
1546
1638
|
try {
|
|
1547
|
-
|
|
1639
|
+
fs4.unlinkSync(markerPath);
|
|
1548
1640
|
} catch {
|
|
1549
1641
|
}
|
|
1550
1642
|
}
|
|
@@ -1552,7 +1644,7 @@ function writeCustomCertMarker(dir, enabled2) {
|
|
|
1552
1644
|
var LAN_MARKER_FILE = "proxy.lan";
|
|
1553
1645
|
function readLanMarker(dir) {
|
|
1554
1646
|
try {
|
|
1555
|
-
const raw =
|
|
1647
|
+
const raw = fs4.readFileSync(path3.join(dir, LAN_MARKER_FILE), "utf-8").trim();
|
|
1556
1648
|
return raw || null;
|
|
1557
1649
|
} catch {
|
|
1558
1650
|
return null;
|
|
@@ -1562,11 +1654,11 @@ function writeLanMarker(dir, ip) {
|
|
|
1562
1654
|
const markerPath = path3.join(dir, LAN_MARKER_FILE);
|
|
1563
1655
|
if (!ip) {
|
|
1564
1656
|
try {
|
|
1565
|
-
|
|
1657
|
+
fs4.unlinkSync(markerPath);
|
|
1566
1658
|
} catch {
|
|
1567
1659
|
}
|
|
1568
1660
|
} else {
|
|
1569
|
-
|
|
1661
|
+
fs4.writeFileSync(markerPath, ip, { mode: 420 });
|
|
1570
1662
|
}
|
|
1571
1663
|
}
|
|
1572
1664
|
var DEFAULT_TLD = "localhost";
|
|
@@ -1610,7 +1702,7 @@ var TLD_FILE = "proxy.tld";
|
|
|
1610
1702
|
var TLDS_FILE = "proxy.tlds";
|
|
1611
1703
|
function readLegacyTldFromDir(dir) {
|
|
1612
1704
|
try {
|
|
1613
|
-
const raw =
|
|
1705
|
+
const raw = fs4.readFileSync(path3.join(dir, TLD_FILE), "utf-8").trim();
|
|
1614
1706
|
return raw || DEFAULT_TLD;
|
|
1615
1707
|
} catch {
|
|
1616
1708
|
return DEFAULT_TLD;
|
|
@@ -1618,7 +1710,7 @@ function readLegacyTldFromDir(dir) {
|
|
|
1618
1710
|
}
|
|
1619
1711
|
function readTldsFromDir(dir) {
|
|
1620
1712
|
try {
|
|
1621
|
-
const raw =
|
|
1713
|
+
const raw = fs4.readFileSync(path3.join(dir, TLDS_FILE), "utf-8").trim();
|
|
1622
1714
|
const parsed = raw.startsWith("[") ? JSON.parse(raw) : raw.split(/\r?\n/).flatMap((line) => line.split(",")).map((line) => line.trim()).filter(Boolean);
|
|
1623
1715
|
if (!Array.isArray(parsed)) return [readLegacyTldFromDir(dir)];
|
|
1624
1716
|
const tlds = parsed.flatMap((value) => typeof value === "string" ? parseTldList(value) : []);
|
|
@@ -1633,16 +1725,16 @@ function writeTldsFile(dir, tlds) {
|
|
|
1633
1725
|
const tldPath = path3.join(dir, TLD_FILE);
|
|
1634
1726
|
if (uniqueTlds.length === 1 && uniqueTlds[0] === DEFAULT_TLD) {
|
|
1635
1727
|
try {
|
|
1636
|
-
|
|
1728
|
+
fs4.unlinkSync(tldsPath);
|
|
1637
1729
|
} catch {
|
|
1638
1730
|
}
|
|
1639
1731
|
try {
|
|
1640
|
-
|
|
1732
|
+
fs4.unlinkSync(tldPath);
|
|
1641
1733
|
} catch {
|
|
1642
1734
|
}
|
|
1643
1735
|
} else {
|
|
1644
|
-
|
|
1645
|
-
|
|
1736
|
+
fs4.writeFileSync(tldsPath, uniqueTlds.join("\n") + "\n", { mode: 420 });
|
|
1737
|
+
fs4.writeFileSync(tldPath, uniqueTlds[0] ?? DEFAULT_TLD, { mode: 420 });
|
|
1646
1738
|
}
|
|
1647
1739
|
}
|
|
1648
1740
|
function writeTldFile(dir, tld) {
|
|
@@ -1957,7 +2049,7 @@ function collectBinPaths(cwd) {
|
|
|
1957
2049
|
let dir = cwd;
|
|
1958
2050
|
for (; ; ) {
|
|
1959
2051
|
const bin = path3.join(dir, "node_modules", ".bin");
|
|
1960
|
-
if (
|
|
2052
|
+
if (fs4.existsSync(bin)) {
|
|
1961
2053
|
dirs.push(bin);
|
|
1962
2054
|
}
|
|
1963
2055
|
const parent = path3.dirname(dir);
|
|
@@ -2088,7 +2180,7 @@ function injectFrameworkFlags(commandArgs, port) {
|
|
|
2088
2180
|
}
|
|
2089
2181
|
|
|
2090
2182
|
// src/clean-utils.ts
|
|
2091
|
-
import * as
|
|
2183
|
+
import * as fs5 from "fs";
|
|
2092
2184
|
import * as path4 from "path";
|
|
2093
2185
|
var PORTLESS_STATE_FILES = [
|
|
2094
2186
|
"routes.json",
|
|
@@ -2101,6 +2193,8 @@ var PORTLESS_STATE_FILES = [
|
|
|
2101
2193
|
"proxy.tld",
|
|
2102
2194
|
"proxy.tlds",
|
|
2103
2195
|
"proxy.lan",
|
|
2196
|
+
"ca.trusted",
|
|
2197
|
+
"ca.trust-refresh-pending",
|
|
2104
2198
|
"ca-key.pem",
|
|
2105
2199
|
"ca.pem",
|
|
2106
2200
|
"server-key.pem",
|
|
@@ -2110,28 +2204,38 @@ var PORTLESS_STATE_FILES = [
|
|
|
2110
2204
|
"ca.srl"
|
|
2111
2205
|
];
|
|
2112
2206
|
var HOST_CERTS_DIR2 = "host-certs";
|
|
2207
|
+
var CA_IDENTITY_FILES = /* @__PURE__ */ new Set(["ca-key.pem", "ca.pem", "ca.trust-refresh-pending"]);
|
|
2113
2208
|
function collectStateDirsForCleanup() {
|
|
2114
2209
|
const dirs = /* @__PURE__ */ new Set();
|
|
2115
2210
|
const add = (d) => {
|
|
2116
2211
|
const trimmed = d?.trim();
|
|
2117
2212
|
if (!trimmed) return;
|
|
2118
2213
|
const resolved = path4.resolve(trimmed);
|
|
2119
|
-
if (
|
|
2214
|
+
if (fs5.existsSync(resolved)) dirs.add(resolved);
|
|
2120
2215
|
};
|
|
2121
2216
|
add(USER_STATE_DIR);
|
|
2122
2217
|
add(LEGACY_SYSTEM_STATE_DIR);
|
|
2123
2218
|
add(process.env.PORTLESS_STATE_DIR);
|
|
2124
2219
|
return [...dirs];
|
|
2125
2220
|
}
|
|
2126
|
-
function
|
|
2221
|
+
function attemptCATrustRemovalForCleanup(stateDirs, untrust) {
|
|
2222
|
+
const results = /* @__PURE__ */ new Map();
|
|
2223
|
+
for (const stateDir of stateDirs) {
|
|
2224
|
+
if (!fs5.existsSync(path4.join(stateDir, "ca.pem"))) continue;
|
|
2225
|
+
results.set(stateDir, untrust(stateDir));
|
|
2226
|
+
}
|
|
2227
|
+
return results;
|
|
2228
|
+
}
|
|
2229
|
+
function removePortlessStateFiles(dir, options = {}) {
|
|
2127
2230
|
for (const f of PORTLESS_STATE_FILES) {
|
|
2231
|
+
if (options.preserveCAIdentity && CA_IDENTITY_FILES.has(f)) continue;
|
|
2128
2232
|
try {
|
|
2129
|
-
|
|
2233
|
+
fs5.unlinkSync(path4.join(dir, f));
|
|
2130
2234
|
} catch {
|
|
2131
2235
|
}
|
|
2132
2236
|
}
|
|
2133
2237
|
try {
|
|
2134
|
-
|
|
2238
|
+
fs5.rmSync(path4.join(dir, HOST_CERTS_DIR2), { recursive: true, force: true });
|
|
2135
2239
|
} catch {
|
|
2136
2240
|
}
|
|
2137
2241
|
}
|
|
@@ -2340,7 +2444,7 @@ function cleanupAll() {
|
|
|
2340
2444
|
}
|
|
2341
2445
|
|
|
2342
2446
|
// src/config.ts
|
|
2343
|
-
import * as
|
|
2447
|
+
import * as fs6 from "fs";
|
|
2344
2448
|
import * as path5 from "path";
|
|
2345
2449
|
var ConfigValidationError = class extends Error {
|
|
2346
2450
|
constructor(message) {
|
|
@@ -2352,7 +2456,7 @@ var CONFIG_FILENAME = "portless.json";
|
|
|
2352
2456
|
function loadConfig(cwd = process.cwd()) {
|
|
2353
2457
|
const configPath = path5.join(cwd, CONFIG_FILENAME);
|
|
2354
2458
|
try {
|
|
2355
|
-
const raw =
|
|
2459
|
+
const raw = fs6.readFileSync(configPath, "utf-8");
|
|
2356
2460
|
const parsed = JSON.parse(raw);
|
|
2357
2461
|
validateConfig(parsed, configPath);
|
|
2358
2462
|
return { config: parsed, configDir: cwd };
|
|
@@ -2375,7 +2479,7 @@ function normalizePortlessValue(value) {
|
|
|
2375
2479
|
function loadConfigFromPackageJson(dir) {
|
|
2376
2480
|
const pkgPath = path5.join(dir, "package.json");
|
|
2377
2481
|
try {
|
|
2378
|
-
const raw =
|
|
2482
|
+
const raw = fs6.readFileSync(pkgPath, "utf-8");
|
|
2379
2483
|
const pkg = JSON.parse(raw);
|
|
2380
2484
|
if (pkg && typeof pkg === "object" && "portless" in pkg) {
|
|
2381
2485
|
const config = normalizePortlessValue(pkg.portless);
|
|
@@ -2393,7 +2497,7 @@ function loadConfigFromPackageJson(dir) {
|
|
|
2393
2497
|
function loadPackagePortlessConfig(dir) {
|
|
2394
2498
|
const pkgPath = path5.join(dir, "package.json");
|
|
2395
2499
|
try {
|
|
2396
|
-
const raw =
|
|
2500
|
+
const raw = fs6.readFileSync(pkgPath, "utf-8");
|
|
2397
2501
|
const pkg = JSON.parse(raw);
|
|
2398
2502
|
if (pkg && typeof pkg === "object" && "portless" in pkg) {
|
|
2399
2503
|
const config = normalizePortlessValue(pkg.portless);
|
|
@@ -2429,7 +2533,7 @@ function resolveAppConfig(config, configDir, packageDir) {
|
|
|
2429
2533
|
function hasScript(scriptName, dir) {
|
|
2430
2534
|
const pkgPath = path5.join(dir, "package.json");
|
|
2431
2535
|
try {
|
|
2432
|
-
const raw =
|
|
2536
|
+
const raw = fs6.readFileSync(pkgPath, "utf-8");
|
|
2433
2537
|
const pkg = JSON.parse(raw);
|
|
2434
2538
|
return typeof pkg?.scripts?.[scriptName] === "string";
|
|
2435
2539
|
} catch {
|
|
@@ -2448,7 +2552,7 @@ function detectPackageManager(cwd) {
|
|
|
2448
2552
|
for (; ; ) {
|
|
2449
2553
|
const pkgPath = path5.join(dir, "package.json");
|
|
2450
2554
|
try {
|
|
2451
|
-
const raw =
|
|
2555
|
+
const raw = fs6.readFileSync(pkgPath, "utf-8");
|
|
2452
2556
|
const pkg = JSON.parse(raw);
|
|
2453
2557
|
if (typeof pkg.packageManager === "string") {
|
|
2454
2558
|
const name = pkg.packageManager.split("@")[0];
|
|
@@ -2460,7 +2564,7 @@ function detectPackageManager(cwd) {
|
|
|
2460
2564
|
}
|
|
2461
2565
|
for (const [file, pm] of LOCK_FILES) {
|
|
2462
2566
|
try {
|
|
2463
|
-
|
|
2567
|
+
fs6.accessSync(path5.join(dir, file), fs6.constants.F_OK);
|
|
2464
2568
|
return pm;
|
|
2465
2569
|
} catch {
|
|
2466
2570
|
}
|
|
@@ -2619,13 +2723,13 @@ function warnUnknownKeys(obj, known, configPath, prefix) {
|
|
|
2619
2723
|
}
|
|
2620
2724
|
|
|
2621
2725
|
// src/workspace.ts
|
|
2622
|
-
import * as
|
|
2726
|
+
import * as fs7 from "fs";
|
|
2623
2727
|
import * as path6 from "path";
|
|
2624
2728
|
function findWorkspaceRoot(cwd = process.cwd()) {
|
|
2625
2729
|
let dir = cwd;
|
|
2626
2730
|
for (; ; ) {
|
|
2627
2731
|
try {
|
|
2628
|
-
|
|
2732
|
+
fs7.accessSync(path6.join(dir, "pnpm-workspace.yaml"), fs7.constants.R_OK);
|
|
2629
2733
|
return dir;
|
|
2630
2734
|
} catch {
|
|
2631
2735
|
}
|
|
@@ -2639,7 +2743,7 @@ function findWorkspaceRoot(cwd = process.cwd()) {
|
|
|
2639
2743
|
}
|
|
2640
2744
|
function detectWorkspaceSource(workspaceRoot) {
|
|
2641
2745
|
try {
|
|
2642
|
-
|
|
2746
|
+
fs7.accessSync(path6.join(workspaceRoot, "pnpm-workspace.yaml"), fs7.constants.R_OK);
|
|
2643
2747
|
return "pnpm";
|
|
2644
2748
|
} catch {
|
|
2645
2749
|
}
|
|
@@ -2651,7 +2755,7 @@ function detectWorkspaceSource(workspaceRoot) {
|
|
|
2651
2755
|
function readWorkspacesFromPackageJson(dir) {
|
|
2652
2756
|
const pkgPath = path6.join(dir, "package.json");
|
|
2653
2757
|
try {
|
|
2654
|
-
const raw =
|
|
2758
|
+
const raw = fs7.readFileSync(pkgPath, "utf-8");
|
|
2655
2759
|
const pkg = JSON.parse(raw);
|
|
2656
2760
|
if (!pkg || typeof pkg !== "object") return null;
|
|
2657
2761
|
const ws = pkg.workspaces;
|
|
@@ -2672,7 +2776,7 @@ function discoverWorkspacePackages(workspaceRoot) {
|
|
|
2672
2776
|
const wsPath = path6.join(workspaceRoot, "pnpm-workspace.yaml");
|
|
2673
2777
|
let content;
|
|
2674
2778
|
try {
|
|
2675
|
-
content =
|
|
2779
|
+
content = fs7.readFileSync(wsPath, "utf-8");
|
|
2676
2780
|
} catch {
|
|
2677
2781
|
return [];
|
|
2678
2782
|
}
|
|
@@ -2687,7 +2791,7 @@ function discoverWorkspacePackages(workspaceRoot) {
|
|
|
2687
2791
|
for (const dir of dirs) {
|
|
2688
2792
|
const pkgPath = path6.join(dir, "package.json");
|
|
2689
2793
|
try {
|
|
2690
|
-
const raw =
|
|
2794
|
+
const raw = fs7.readFileSync(pkgPath, "utf-8");
|
|
2691
2795
|
const pkg = JSON.parse(raw);
|
|
2692
2796
|
const rawName = typeof pkg.name === "string" ? pkg.name : null;
|
|
2693
2797
|
const scopeMatch = rawName?.match(/^@([^/]+)\//);
|
|
@@ -2769,7 +2873,7 @@ function expandSingleGlob(root, glob) {
|
|
|
2769
2873
|
function expandSegments(base, segments) {
|
|
2770
2874
|
if (segments.length === 0) {
|
|
2771
2875
|
try {
|
|
2772
|
-
const stat =
|
|
2876
|
+
const stat = fs7.statSync(base);
|
|
2773
2877
|
if (stat.isDirectory()) return [base];
|
|
2774
2878
|
} catch {
|
|
2775
2879
|
}
|
|
@@ -2778,7 +2882,7 @@ function expandSegments(base, segments) {
|
|
|
2778
2882
|
const [current, ...rest] = segments;
|
|
2779
2883
|
if (current.includes("*")) {
|
|
2780
2884
|
try {
|
|
2781
|
-
const entries =
|
|
2885
|
+
const entries = fs7.readdirSync(base, { withFileTypes: true });
|
|
2782
2886
|
const matched = entries.filter((e) => e.isDirectory() && segmentMatches(current, e.name));
|
|
2783
2887
|
if (rest.length === 0) {
|
|
2784
2888
|
return matched.map((e) => path6.join(base, e.name));
|
|
@@ -2796,7 +2900,7 @@ function expandSegments(base, segments) {
|
|
|
2796
2900
|
}
|
|
2797
2901
|
|
|
2798
2902
|
// src/turbo.ts
|
|
2799
|
-
import * as
|
|
2903
|
+
import * as fs8 from "fs";
|
|
2800
2904
|
import * as path7 from "path";
|
|
2801
2905
|
var LOADER_FILENAME = "turbo-env-loader.cjs";
|
|
2802
2906
|
var MANIFEST_FILENAME = "dev-manifest.json";
|
|
@@ -2826,23 +2930,23 @@ try {
|
|
|
2826
2930
|
`;
|
|
2827
2931
|
}
|
|
2828
2932
|
function ensureEnvLoader(baseDir = USER_STATE_DIR) {
|
|
2829
|
-
|
|
2933
|
+
fs8.mkdirSync(baseDir, { recursive: true, mode: 493 });
|
|
2830
2934
|
const target = loaderPath(baseDir);
|
|
2831
2935
|
const source = loaderSource(baseDir);
|
|
2832
2936
|
try {
|
|
2833
|
-
const existing =
|
|
2937
|
+
const existing = fs8.readFileSync(target, "utf-8");
|
|
2834
2938
|
if (existing === source) return;
|
|
2835
2939
|
} catch {
|
|
2836
2940
|
}
|
|
2837
|
-
|
|
2941
|
+
fs8.writeFileSync(target, source, { mode: 420 });
|
|
2838
2942
|
}
|
|
2839
2943
|
function writeManifest(entries, baseDir = USER_STATE_DIR) {
|
|
2840
|
-
|
|
2841
|
-
|
|
2944
|
+
fs8.mkdirSync(baseDir, { recursive: true, mode: 493 });
|
|
2945
|
+
fs8.writeFileSync(manifestPath(baseDir), JSON.stringify(entries, null, 2) + "\n", { mode: 420 });
|
|
2842
2946
|
}
|
|
2843
2947
|
function removeManifest(baseDir = USER_STATE_DIR) {
|
|
2844
2948
|
try {
|
|
2845
|
-
|
|
2949
|
+
fs8.unlinkSync(manifestPath(baseDir));
|
|
2846
2950
|
} catch {
|
|
2847
2951
|
}
|
|
2848
2952
|
}
|
|
@@ -2854,7 +2958,7 @@ function buildNodeOptions(baseDir = USER_STATE_DIR) {
|
|
|
2854
2958
|
}
|
|
2855
2959
|
function hasTurboConfig(wsRoot) {
|
|
2856
2960
|
try {
|
|
2857
|
-
|
|
2961
|
+
fs8.accessSync(path7.join(wsRoot, "turbo.json"), fs8.constants.R_OK);
|
|
2858
2962
|
return true;
|
|
2859
2963
|
} catch {
|
|
2860
2964
|
return false;
|
|
@@ -2862,8 +2966,8 @@ function hasTurboConfig(wsRoot) {
|
|
|
2862
2966
|
}
|
|
2863
2967
|
|
|
2864
2968
|
// src/service.ts
|
|
2865
|
-
import * as
|
|
2866
|
-
import * as
|
|
2969
|
+
import * as fs9 from "fs";
|
|
2970
|
+
import * as os3 from "os";
|
|
2867
2971
|
import * as path8 from "path";
|
|
2868
2972
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
2869
2973
|
var DEFAULT_SERVICE_PORT = getProtocolPort(true);
|
|
@@ -2894,7 +2998,7 @@ var DEFAULT_SERVICE_CONFIG = {
|
|
|
2894
2998
|
useWildcard: false,
|
|
2895
2999
|
extraEnv: {}
|
|
2896
3000
|
};
|
|
2897
|
-
function
|
|
3001
|
+
function defaultRunner4(command, args, options) {
|
|
2898
3002
|
return spawnSync4(command, args, {
|
|
2899
3003
|
encoding: "utf-8",
|
|
2900
3004
|
stdio: options?.stdio ?? "pipe"
|
|
@@ -2936,7 +3040,7 @@ function getFlagValue(args, index, flag) {
|
|
|
2936
3040
|
return value;
|
|
2937
3041
|
}
|
|
2938
3042
|
function resolveServicePath(value) {
|
|
2939
|
-
const expanded = value === "~" ?
|
|
3043
|
+
const expanded = value === "~" ? os3.homedir() : value.startsWith("~/") || value.startsWith("~\\") ? path8.join(os3.homedir(), value.slice(2)) : value;
|
|
2940
3044
|
return path8.resolve(expanded);
|
|
2941
3045
|
}
|
|
2942
3046
|
function normalizeServiceInstallPaths(config) {
|
|
@@ -3064,34 +3168,21 @@ function parseServiceInstallConfig(args, env = process.env, options = {}) {
|
|
|
3064
3168
|
}
|
|
3065
3169
|
return config;
|
|
3066
3170
|
}
|
|
3067
|
-
function readPasswdHome(username) {
|
|
3068
|
-
try {
|
|
3069
|
-
const passwd = fs8.readFileSync("/etc/passwd", "utf-8");
|
|
3070
|
-
for (const line of passwd.split("\n")) {
|
|
3071
|
-
const fields = line.split(":");
|
|
3072
|
-
if (fields[0] === username && fields[5]) {
|
|
3073
|
-
return fields[5];
|
|
3074
|
-
}
|
|
3075
|
-
}
|
|
3076
|
-
} catch {
|
|
3077
|
-
}
|
|
3078
|
-
return null;
|
|
3079
|
-
}
|
|
3080
3171
|
function resolveUserContext(platform) {
|
|
3081
3172
|
if (platform === "win32") {
|
|
3082
|
-
const home =
|
|
3173
|
+
const home = resolveUserHome({ platform });
|
|
3083
3174
|
return { home, username: process.env.USERNAME };
|
|
3084
3175
|
}
|
|
3085
3176
|
const sudoUser = process.env.SUDO_USER;
|
|
3086
3177
|
const sudoUid = process.env.SUDO_UID;
|
|
3087
3178
|
const sudoGid = process.env.SUDO_GID;
|
|
3088
3179
|
if (sudoUser && sudoUser !== "root") {
|
|
3089
|
-
const home =
|
|
3180
|
+
const home = resolveUserHome({ platform });
|
|
3090
3181
|
return { home, uid: sudoUid, gid: sudoGid, username: sudoUser };
|
|
3091
3182
|
}
|
|
3092
|
-
const userInfo2 =
|
|
3183
|
+
const userInfo2 = os3.userInfo();
|
|
3093
3184
|
return {
|
|
3094
|
-
home:
|
|
3185
|
+
home: os3.homedir(),
|
|
3095
3186
|
uid: process.getuid?.()?.toString(),
|
|
3096
3187
|
gid: process.getgid?.()?.toString(),
|
|
3097
3188
|
username: userInfo2.username
|
|
@@ -3369,8 +3460,8 @@ function parsePlistEnv(block) {
|
|
|
3369
3460
|
function readInstalledServiceSnapshot(spec) {
|
|
3370
3461
|
try {
|
|
3371
3462
|
if (spec.platform === "darwin") {
|
|
3372
|
-
if (!
|
|
3373
|
-
const plist =
|
|
3463
|
+
if (!fs9.existsSync(spec.plistPath)) return null;
|
|
3464
|
+
const plist = fs9.readFileSync(spec.plistPath, "utf-8");
|
|
3374
3465
|
const argsBlock = plist.match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/);
|
|
3375
3466
|
const envBlock = plist.match(/<key>EnvironmentVariables<\/key>\s*<dict>([\s\S]*?)<\/dict>/);
|
|
3376
3467
|
if (!argsBlock) return null;
|
|
@@ -3380,8 +3471,8 @@ function readInstalledServiceSnapshot(spec) {
|
|
|
3380
3471
|
};
|
|
3381
3472
|
}
|
|
3382
3473
|
if (spec.platform === "linux") {
|
|
3383
|
-
if (!
|
|
3384
|
-
const unit =
|
|
3474
|
+
if (!fs9.existsSync(spec.unitPath)) return null;
|
|
3475
|
+
const unit = fs9.readFileSync(spec.unitPath, "utf-8");
|
|
3385
3476
|
const env2 = {};
|
|
3386
3477
|
let command = null;
|
|
3387
3478
|
for (const line of unit.split("\n")) {
|
|
@@ -3399,8 +3490,8 @@ function readInstalledServiceSnapshot(spec) {
|
|
|
3399
3490
|
}
|
|
3400
3491
|
return command ? { command, env: env2 } : null;
|
|
3401
3492
|
}
|
|
3402
|
-
if (!
|
|
3403
|
-
const script =
|
|
3493
|
+
if (!fs9.existsSync(spec.scriptPath)) return null;
|
|
3494
|
+
const script = fs9.readFileSync(spec.scriptPath, "utf-8");
|
|
3404
3495
|
const env = {};
|
|
3405
3496
|
let commandLine = null;
|
|
3406
3497
|
for (const rawLine of script.split(/\r?\n/)) {
|
|
@@ -3462,7 +3553,7 @@ function buildElevatedEnvArgs(options) {
|
|
|
3462
3553
|
}
|
|
3463
3554
|
function buildServiceUninstallSudoArgs(entryScript, options = {}) {
|
|
3464
3555
|
const env = options.env ?? process.env;
|
|
3465
|
-
const home = options.home ??
|
|
3556
|
+
const home = options.home ?? os3.homedir();
|
|
3466
3557
|
const stateDir = options.stateDir ?? env.PORTLESS_STATE_DIR ?? path8.join(home, ".portless");
|
|
3467
3558
|
return [
|
|
3468
3559
|
...buildElevatedEnvArgs({ home, stateDir, env }),
|
|
@@ -3476,7 +3567,7 @@ function requireUnixElevation(args, runner) {
|
|
|
3476
3567
|
if (process.platform !== "darwin" && process.platform !== "linux") return;
|
|
3477
3568
|
if ((process.getuid?.() ?? -1) === 0) return;
|
|
3478
3569
|
if (process.env[INTERNAL_ELEVATED_ENV] === "1") return;
|
|
3479
|
-
const home =
|
|
3570
|
+
const home = os3.homedir();
|
|
3480
3571
|
const stateDir = process.env.PORTLESS_STATE_DIR || path8.join(home, ".portless");
|
|
3481
3572
|
const result = runner(
|
|
3482
3573
|
"sudo",
|
|
@@ -3533,7 +3624,7 @@ async function stopExistingProxy(entryScript, runner, proxyPort) {
|
|
|
3533
3624
|
}
|
|
3534
3625
|
}
|
|
3535
3626
|
function prepareServiceState(stateDir) {
|
|
3536
|
-
|
|
3627
|
+
fs9.mkdirSync(stateDir, { recursive: true });
|
|
3537
3628
|
fixOwnership(stateDir);
|
|
3538
3629
|
}
|
|
3539
3630
|
function prepareTrust(stateDir) {
|
|
@@ -3581,8 +3672,8 @@ async function installService(entryScript, runner, args) {
|
|
|
3581
3672
|
if (spec.platform === "darwin") {
|
|
3582
3673
|
runOptional(runner, "launchctl", ["bootout", "system", spec.plistPath]);
|
|
3583
3674
|
await stopExistingProxy(entryScript, runner, spec.config.proxyPort);
|
|
3584
|
-
|
|
3585
|
-
|
|
3675
|
+
fs9.writeFileSync(spec.plistPath, spec.plist);
|
|
3676
|
+
fs9.chmodSync(spec.plistPath, 420);
|
|
3586
3677
|
runRequired(runner, "chown", ["root:wheel", spec.plistPath]);
|
|
3587
3678
|
runRequired(runner, "launchctl", ["bootstrap", "system", spec.plistPath]);
|
|
3588
3679
|
runRequired(runner, "launchctl", ["enable", `system/${spec.label}`]);
|
|
@@ -3590,16 +3681,16 @@ async function installService(entryScript, runner, args) {
|
|
|
3590
3681
|
} else if (spec.platform === "linux") {
|
|
3591
3682
|
runOptional(runner, "systemctl", ["disable", "--now", spec.serviceName]);
|
|
3592
3683
|
await stopExistingProxy(entryScript, runner, spec.config.proxyPort);
|
|
3593
|
-
|
|
3594
|
-
|
|
3684
|
+
fs9.writeFileSync(spec.unitPath, spec.unit);
|
|
3685
|
+
fs9.chmodSync(spec.unitPath, 420);
|
|
3595
3686
|
runRequired(runner, "systemctl", ["daemon-reload"]);
|
|
3596
3687
|
runRequired(runner, "systemctl", ["enable", spec.serviceName]);
|
|
3597
3688
|
runRequired(runner, "systemctl", ["restart", spec.serviceName]);
|
|
3598
3689
|
} else {
|
|
3599
3690
|
runOptional(runner, "schtasks", ["/End", "/TN", spec.taskName]);
|
|
3600
3691
|
await stopExistingProxy(entryScript, runner, spec.config.proxyPort);
|
|
3601
|
-
|
|
3602
|
-
|
|
3692
|
+
fs9.mkdirSync(spec.scriptDir, { recursive: true });
|
|
3693
|
+
fs9.writeFileSync(spec.scriptPath, spec.script);
|
|
3603
3694
|
runRequired(runner, "schtasks", spec.createArgs);
|
|
3604
3695
|
runOptional(runner, "schtasks", spec.runArgs);
|
|
3605
3696
|
}
|
|
@@ -3612,32 +3703,32 @@ async function uninstallService(entryScript, runner) {
|
|
|
3612
3703
|
const spec = currentServiceSpec(entryScript);
|
|
3613
3704
|
if (spec.platform === "darwin") {
|
|
3614
3705
|
runOptional(runner, "launchctl", ["bootout", "system", spec.plistPath]);
|
|
3615
|
-
|
|
3706
|
+
fs9.rmSync(spec.plistPath, { force: true });
|
|
3616
3707
|
} else if (spec.platform === "linux") {
|
|
3617
3708
|
runOptional(runner, "systemctl", ["disable", "--now", spec.serviceName]);
|
|
3618
|
-
|
|
3709
|
+
fs9.rmSync(spec.unitPath, { force: true });
|
|
3619
3710
|
runOptional(runner, "systemctl", ["daemon-reload"]);
|
|
3620
3711
|
} else {
|
|
3621
3712
|
runOptional(runner, "schtasks", ["/End", "/TN", spec.taskName]);
|
|
3622
3713
|
runOptional(runner, "schtasks", spec.deleteArgs);
|
|
3623
|
-
|
|
3714
|
+
fs9.rmSync(spec.scriptDir, { recursive: true, force: true });
|
|
3624
3715
|
}
|
|
3625
3716
|
console.log(colors_default.green("Portless service uninstalled."));
|
|
3626
3717
|
}
|
|
3627
|
-
function tryUninstallService(entryScript, runner =
|
|
3718
|
+
function tryUninstallService(entryScript, runner = defaultRunner4) {
|
|
3628
3719
|
let installed = false;
|
|
3629
3720
|
try {
|
|
3630
3721
|
const spec = currentServiceSpec(entryScript);
|
|
3631
3722
|
if (spec.platform === "darwin") {
|
|
3632
|
-
installed =
|
|
3723
|
+
installed = fs9.existsSync(spec.plistPath);
|
|
3633
3724
|
if (!installed) return { removed: false, installed: false };
|
|
3634
3725
|
runOptional(runner, "launchctl", ["bootout", "system", spec.plistPath]);
|
|
3635
|
-
|
|
3726
|
+
fs9.rmSync(spec.plistPath, { force: true });
|
|
3636
3727
|
} else if (spec.platform === "linux") {
|
|
3637
|
-
installed =
|
|
3728
|
+
installed = fs9.existsSync(spec.unitPath);
|
|
3638
3729
|
if (!installed) return { removed: false, installed: false };
|
|
3639
3730
|
runOptional(runner, "systemctl", ["disable", "--now", spec.serviceName]);
|
|
3640
|
-
|
|
3731
|
+
fs9.rmSync(spec.unitPath, { force: true });
|
|
3641
3732
|
runOptional(runner, "systemctl", ["daemon-reload"]);
|
|
3642
3733
|
} else {
|
|
3643
3734
|
const query = runner("schtasks", ["/Query", "/TN", spec.taskName, "/FO", "LIST"]);
|
|
@@ -3645,7 +3736,7 @@ function tryUninstallService(entryScript, runner = defaultRunner3) {
|
|
|
3645
3736
|
if (!installed) return { removed: false, installed: false };
|
|
3646
3737
|
runOptional(runner, "schtasks", ["/End", "/TN", spec.taskName]);
|
|
3647
3738
|
runRequired(runner, "schtasks", spec.deleteArgs);
|
|
3648
|
-
|
|
3739
|
+
fs9.rmSync(spec.scriptDir, { recursive: true, force: true });
|
|
3649
3740
|
}
|
|
3650
3741
|
return { removed: true, installed: true };
|
|
3651
3742
|
} catch (err) {
|
|
@@ -3662,7 +3753,7 @@ async function getServiceStatus(entryScript, runner) {
|
|
|
3662
3753
|
const installedConfig = readInstalledServiceConfig(spec) ?? spec.config;
|
|
3663
3754
|
const proxyRunning = await isProxyRunning(installedConfig.proxyPort, installedConfig.useHttps);
|
|
3664
3755
|
if (spec.platform === "darwin") {
|
|
3665
|
-
const installed2 =
|
|
3756
|
+
const installed2 = fs9.existsSync(spec.plistPath);
|
|
3666
3757
|
const result = runner("launchctl", ["print", `system/${spec.label}`]);
|
|
3667
3758
|
const output2 = `${result.stdout || ""}${result.stderr || ""}`;
|
|
3668
3759
|
const managerState = result.status === 0 && /state = running|pid = \d+/.test(output2) ? "running" : installed2 ? "installed" : "not installed";
|
|
@@ -3677,7 +3768,7 @@ async function getServiceStatus(entryScript, runner) {
|
|
|
3677
3768
|
if (spec.platform === "linux") {
|
|
3678
3769
|
const enabled2 = runner("systemctl", ["is-enabled", spec.serviceName]);
|
|
3679
3770
|
const active = runner("systemctl", ["is-active", spec.serviceName]);
|
|
3680
|
-
const installed2 = enabled2.status === 0 || active.status === 0 ||
|
|
3771
|
+
const installed2 = enabled2.status === 0 || active.status === 0 || fs9.existsSync(spec.unitPath);
|
|
3681
3772
|
const activeText = (active.stdout || "").trim();
|
|
3682
3773
|
return {
|
|
3683
3774
|
installed: installed2,
|
|
@@ -3752,7 +3843,7 @@ ${colors_default.bold("Notes:")}
|
|
|
3752
3843
|
}
|
|
3753
3844
|
async function handleService(args, options) {
|
|
3754
3845
|
const action = args[1];
|
|
3755
|
-
const runner = options.runner ||
|
|
3846
|
+
const runner = options.runner || defaultRunner4;
|
|
3756
3847
|
if (!action || action === "--help" || action === "-h") {
|
|
3757
3848
|
printServiceHelp();
|
|
3758
3849
|
process.exit(0);
|
|
@@ -3949,7 +4040,7 @@ function getEntryScript() {
|
|
|
3949
4040
|
function isLocallyInstalled() {
|
|
3950
4041
|
let dir = process.cwd();
|
|
3951
4042
|
for (; ; ) {
|
|
3952
|
-
if (
|
|
4043
|
+
if (fs10.existsSync(path9.join(dir, "node_modules", "portless", "package.json"))) {
|
|
3953
4044
|
return true;
|
|
3954
4045
|
}
|
|
3955
4046
|
const parent = path9.dirname(dir);
|
|
@@ -4021,6 +4112,12 @@ function formatUrls(hostnames, proxyPort, tls2) {
|
|
|
4021
4112
|
function formatViteAllowedHosts(tlds) {
|
|
4022
4113
|
return tlds.map((configuredTld) => `.${configuredTld}`).join(",");
|
|
4023
4114
|
}
|
|
4115
|
+
function formatBindEndpoint(host, port) {
|
|
4116
|
+
return host.includes(":") ? `[${host}]:${port}` : `${host}:${port}`;
|
|
4117
|
+
}
|
|
4118
|
+
function isUnavailableIpv6Bind(err, host) {
|
|
4119
|
+
return host.includes(":") && (err.code === "EAFNOSUPPORT" || err.code === "EADDRNOTAVAIL");
|
|
4120
|
+
}
|
|
4024
4121
|
function addRoutes(store, hostnames, port, pid, force = false) {
|
|
4025
4122
|
const registered = [];
|
|
4026
4123
|
const killedPids = [];
|
|
@@ -4056,6 +4153,9 @@ function startProxyServer(store, proxyPort, tld, tlds, tlsOptions, lanIp, strict
|
|
|
4056
4153
|
const isTls = !!tlsOptions;
|
|
4057
4154
|
const mdnsSupport = isMdnsSupported();
|
|
4058
4155
|
let activeLanIp = lanIp && mdnsSupport.supported ? lanIp : null;
|
|
4156
|
+
const lanModeActive = activeLanIp !== null;
|
|
4157
|
+
const bindTargets = getProxyBindTargets(lanModeActive);
|
|
4158
|
+
const primaryBindTarget = bindTargets[0];
|
|
4059
4159
|
const lanIpPinned = !!process.env.PORTLESS_LAN_IP;
|
|
4060
4160
|
let lanMonitor = null;
|
|
4061
4161
|
if (lanIp && !mdnsSupport.supported) {
|
|
@@ -4063,11 +4163,11 @@ function startProxyServer(store, proxyPort, tld, tlds, tlsOptions, lanIp, strict
|
|
|
4063
4163
|
console.warn(chalk.yellow(`LAN mode disabled: ${reason}`));
|
|
4064
4164
|
}
|
|
4065
4165
|
const routesPath = store.getRoutesPath();
|
|
4066
|
-
if (!
|
|
4067
|
-
|
|
4166
|
+
if (!fs10.existsSync(routesPath)) {
|
|
4167
|
+
fs10.writeFileSync(routesPath, "[]", { mode: FILE_MODE });
|
|
4068
4168
|
}
|
|
4069
4169
|
try {
|
|
4070
|
-
|
|
4170
|
+
fs10.chmodSync(routesPath, FILE_MODE);
|
|
4071
4171
|
} catch {
|
|
4072
4172
|
}
|
|
4073
4173
|
fixOwnership(routesPath);
|
|
@@ -4127,7 +4227,7 @@ function startProxyServer(store, proxyPort, tld, tlds, tlsOptions, lanIp, strict
|
|
|
4127
4227
|
}
|
|
4128
4228
|
};
|
|
4129
4229
|
try {
|
|
4130
|
-
watcher =
|
|
4230
|
+
watcher = fs10.watch(routesPath, () => {
|
|
4131
4231
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
4132
4232
|
debounceTimer = setTimeout(reloadRoutes, DEBOUNCE_MS);
|
|
4133
4233
|
});
|
|
@@ -4139,7 +4239,7 @@ function startProxyServer(store, proxyPort, tld, tlds, tlsOptions, lanIp, strict
|
|
|
4139
4239
|
syncHostsFile(cachedRoutes.map((r) => r.hostname));
|
|
4140
4240
|
}
|
|
4141
4241
|
publishCachedRoutes();
|
|
4142
|
-
const
|
|
4242
|
+
const createServer2 = () => createProxyServer({
|
|
4143
4243
|
getRoutes: () => cachedRoutes,
|
|
4144
4244
|
proxyPort,
|
|
4145
4245
|
tld,
|
|
@@ -4148,6 +4248,17 @@ function startProxyServer(store, proxyPort, tld, tlds, tlsOptions, lanIp, strict
|
|
|
4148
4248
|
onError: (msg) => console.error(colors_default.red(msg)),
|
|
4149
4249
|
tls: tlsOptions
|
|
4150
4250
|
});
|
|
4251
|
+
const server = createServer2();
|
|
4252
|
+
const additionalServers = /* @__PURE__ */ new Set();
|
|
4253
|
+
const redirectServers = /* @__PURE__ */ new Set();
|
|
4254
|
+
const closeAuxiliaryServers = () => {
|
|
4255
|
+
for (const auxiliaryServer of [...additionalServers, ...redirectServers]) {
|
|
4256
|
+
try {
|
|
4257
|
+
auxiliaryServer.close();
|
|
4258
|
+
} catch {
|
|
4259
|
+
}
|
|
4260
|
+
}
|
|
4261
|
+
};
|
|
4151
4262
|
server.on("error", (err) => {
|
|
4152
4263
|
if (err.code === "EADDRINUSE") {
|
|
4153
4264
|
console.error(colors_default.red(`Port ${proxyPort} is already in use.`));
|
|
@@ -4166,30 +4277,61 @@ function startProxyServer(store, proxyPort, tld, tlds, tlsOptions, lanIp, strict
|
|
|
4166
4277
|
} else {
|
|
4167
4278
|
console.error(colors_default.red(`Proxy error: ${err.message}`));
|
|
4168
4279
|
}
|
|
4169
|
-
|
|
4280
|
+
closeAuxiliaryServers();
|
|
4170
4281
|
process.exit(1);
|
|
4171
4282
|
});
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4283
|
+
const proto = isTls ? "HTTPS/2" : "HTTP";
|
|
4284
|
+
const tldLabel = tlds.length > 1 || tld !== DEFAULT_TLD ? ` (TLDs: ${formatTldList2(tlds)})` : "";
|
|
4285
|
+
const modeLabel = strict === false ? " (wildcard)" : "";
|
|
4286
|
+
for (const bindTarget of bindTargets.slice(1)) {
|
|
4287
|
+
const additionalServer = createServer2();
|
|
4288
|
+
additionalServers.add(additionalServer);
|
|
4289
|
+
additionalServer.on("error", (err) => {
|
|
4290
|
+
additionalServers.delete(additionalServer);
|
|
4291
|
+
if (!isUnavailableIpv6Bind(err, bindTarget.host)) {
|
|
4292
|
+
console.warn(
|
|
4293
|
+
colors_default.yellow(
|
|
4294
|
+
`Could not listen on ${formatBindEndpoint(bindTarget.host, proxyPort)}: ${err.message}`
|
|
4295
|
+
)
|
|
4296
|
+
);
|
|
4297
|
+
}
|
|
4298
|
+
});
|
|
4299
|
+
listenOnProxyInterface(additionalServer, proxyPort, bindTarget, () => {
|
|
4300
|
+
console.log(
|
|
4301
|
+
colors_default.green(
|
|
4302
|
+
`${proto} proxy listening on ${formatBindEndpoint(bindTarget.host, proxyPort)}${tldLabel}${modeLabel}`
|
|
4303
|
+
)
|
|
4304
|
+
);
|
|
4177
4305
|
});
|
|
4178
|
-
redirectServer.listen(80);
|
|
4179
4306
|
}
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4307
|
+
if (isTls && proxyPort !== 80) {
|
|
4308
|
+
for (const bindTarget of bindTargets) {
|
|
4309
|
+
const redirectServer = createHttpRedirectServer(proxyPort);
|
|
4310
|
+
redirectServers.add(redirectServer);
|
|
4311
|
+
redirectServer.on("error", () => {
|
|
4312
|
+
redirectServers.delete(redirectServer);
|
|
4313
|
+
});
|
|
4314
|
+
listenOnProxyInterface(redirectServer, 80, bindTarget, () => {
|
|
4315
|
+
console.log(
|
|
4316
|
+
colors_default.green(
|
|
4317
|
+
`HTTP-to-HTTPS redirect listening on ${formatBindEndpoint(bindTarget.host, 80)}`
|
|
4318
|
+
)
|
|
4319
|
+
);
|
|
4320
|
+
});
|
|
4321
|
+
}
|
|
4322
|
+
}
|
|
4323
|
+
listenOnProxyInterface(server, proxyPort, primaryBindTarget, () => {
|
|
4324
|
+
fs10.writeFileSync(store.pidPath, process.pid.toString(), { mode: FILE_MODE });
|
|
4325
|
+
fs10.writeFileSync(store.portFilePath, proxyPort.toString(), { mode: FILE_MODE });
|
|
4183
4326
|
writeTlsMarker(store.dir, isTls);
|
|
4184
4327
|
writeCustomCertMarker(store.dir, isTls && customCert);
|
|
4185
4328
|
writeTldsFile(store.dir, tlds);
|
|
4186
4329
|
writeLanMarker(store.dir, activeLanIp);
|
|
4187
4330
|
fixOwnership(store.dir, store.pidPath, store.portFilePath);
|
|
4188
|
-
const proto = isTls ? "HTTPS/2" : "HTTP";
|
|
4189
|
-
const tldLabel = tlds.length > 1 || tld !== DEFAULT_TLD ? ` (TLDs: ${formatTldList2(tlds)})` : "";
|
|
4190
|
-
const modeLabel = strict === false ? " (wildcard)" : "";
|
|
4191
4331
|
console.log(
|
|
4192
|
-
colors_default.green(
|
|
4332
|
+
colors_default.green(
|
|
4333
|
+
`${proto} proxy listening on ${formatBindEndpoint(primaryBindTarget.host, proxyPort)}${tldLabel}${modeLabel}`
|
|
4334
|
+
)
|
|
4193
4335
|
);
|
|
4194
4336
|
if (activeLanIp) {
|
|
4195
4337
|
console.log(chalk.green(`LAN mode: ${activeLanIp}`));
|
|
@@ -4209,9 +4351,6 @@ function startProxyServer(store, proxyPort, tld, tlds, tlsOptions, lanIp, strict
|
|
|
4209
4351
|
});
|
|
4210
4352
|
}
|
|
4211
4353
|
}
|
|
4212
|
-
if (redirectServer) {
|
|
4213
|
-
console.log(colors_default.green("HTTP-to-HTTPS redirect listening on port 80"));
|
|
4214
|
-
}
|
|
4215
4354
|
});
|
|
4216
4355
|
let exiting = false;
|
|
4217
4356
|
const cleanup = () => {
|
|
@@ -4224,15 +4363,13 @@ function startProxyServer(store, proxyPort, tld, tlds, tlsOptions, lanIp, strict
|
|
|
4224
4363
|
watcher.close();
|
|
4225
4364
|
}
|
|
4226
4365
|
if (activeLanIp) cleanupAll();
|
|
4227
|
-
|
|
4228
|
-
redirectServer.close();
|
|
4229
|
-
}
|
|
4366
|
+
closeAuxiliaryServers();
|
|
4230
4367
|
try {
|
|
4231
|
-
|
|
4368
|
+
fs10.unlinkSync(store.pidPath);
|
|
4232
4369
|
} catch {
|
|
4233
4370
|
}
|
|
4234
4371
|
try {
|
|
4235
|
-
|
|
4372
|
+
fs10.unlinkSync(store.portFilePath);
|
|
4236
4373
|
} catch {
|
|
4237
4374
|
}
|
|
4238
4375
|
writeTlsMarker(store.dir, false);
|
|
@@ -4263,7 +4400,7 @@ function sudoStopOrHint(port) {
|
|
|
4263
4400
|
}
|
|
4264
4401
|
async function stopProxy(store, proxyPort, _tls) {
|
|
4265
4402
|
const pidPath = store.pidPath;
|
|
4266
|
-
if (!
|
|
4403
|
+
if (!fs10.existsSync(pidPath)) {
|
|
4267
4404
|
if (await isProxyRunning(proxyPort)) {
|
|
4268
4405
|
console.log(colors_default.yellow(`PID file is missing but port ${proxyPort} is still in use.`));
|
|
4269
4406
|
const pid = findPidOnPort(proxyPort);
|
|
@@ -4271,7 +4408,7 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
4271
4408
|
try {
|
|
4272
4409
|
process.kill(pid, "SIGTERM");
|
|
4273
4410
|
try {
|
|
4274
|
-
|
|
4411
|
+
fs10.unlinkSync(store.portFilePath);
|
|
4275
4412
|
} catch {
|
|
4276
4413
|
}
|
|
4277
4414
|
writeTlsMarker(store.dir, false);
|
|
@@ -4310,10 +4447,10 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
4310
4447
|
return;
|
|
4311
4448
|
}
|
|
4312
4449
|
try {
|
|
4313
|
-
const pid = parseInt(
|
|
4450
|
+
const pid = parseInt(fs10.readFileSync(pidPath, "utf-8"), 10);
|
|
4314
4451
|
if (isNaN(pid)) {
|
|
4315
4452
|
console.error(colors_default.red("Corrupted PID file. Removing it."));
|
|
4316
|
-
|
|
4453
|
+
fs10.unlinkSync(pidPath);
|
|
4317
4454
|
writeTlsMarker(store.dir, false);
|
|
4318
4455
|
writeCustomCertMarker(store.dir, false);
|
|
4319
4456
|
writeTldFile(store.dir, DEFAULT_TLD);
|
|
@@ -4328,9 +4465,9 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
4328
4465
|
return;
|
|
4329
4466
|
}
|
|
4330
4467
|
console.log(colors_default.yellow("Proxy process is no longer running. Cleaning up stale files."));
|
|
4331
|
-
|
|
4468
|
+
fs10.unlinkSync(pidPath);
|
|
4332
4469
|
try {
|
|
4333
|
-
|
|
4470
|
+
fs10.unlinkSync(store.portFilePath);
|
|
4334
4471
|
} catch {
|
|
4335
4472
|
}
|
|
4336
4473
|
writeTlsMarker(store.dir, false);
|
|
@@ -4346,7 +4483,7 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
4346
4483
|
)
|
|
4347
4484
|
);
|
|
4348
4485
|
console.log(colors_default.yellow("Removing stale PID file."));
|
|
4349
|
-
|
|
4486
|
+
fs10.unlinkSync(pidPath);
|
|
4350
4487
|
writeTlsMarker(store.dir, false);
|
|
4351
4488
|
writeCustomCertMarker(store.dir, false);
|
|
4352
4489
|
writeTldFile(store.dir, DEFAULT_TLD);
|
|
@@ -4354,9 +4491,9 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
4354
4491
|
return;
|
|
4355
4492
|
}
|
|
4356
4493
|
process.kill(pid, "SIGTERM");
|
|
4357
|
-
|
|
4494
|
+
fs10.unlinkSync(pidPath);
|
|
4358
4495
|
try {
|
|
4359
|
-
|
|
4496
|
+
fs10.unlinkSync(store.portFilePath);
|
|
4360
4497
|
} catch {
|
|
4361
4498
|
}
|
|
4362
4499
|
writeTlsMarker(store.dir, false);
|
|
@@ -4507,7 +4644,7 @@ async function ensureProxyRunning(proxyPort, tls2, desired) {
|
|
|
4507
4644
|
const logPath = path9.join(fallbackDir, "proxy.log");
|
|
4508
4645
|
console.error(colors_default.blue("Try starting it manually:"));
|
|
4509
4646
|
console.error(colors_default.cyan(` ${manualStartCommand}`));
|
|
4510
|
-
if (
|
|
4647
|
+
if (fs10.existsSync(logPath)) {
|
|
4511
4648
|
console.error(colors_default.gray(`Logs: ${logPath}`));
|
|
4512
4649
|
}
|
|
4513
4650
|
process.exit(1);
|
|
@@ -4772,7 +4909,7 @@ portless
|
|
|
4772
4909
|
const caEnv = {};
|
|
4773
4910
|
if (tls2 && !process.env.NODE_EXTRA_CA_CERTS) {
|
|
4774
4911
|
const caPath = path9.join(stateDir, "ca.pem");
|
|
4775
|
-
if (
|
|
4912
|
+
if (fs10.existsSync(caPath)) {
|
|
4776
4913
|
caEnv.NODE_EXTRA_CA_CERTS = caPath;
|
|
4777
4914
|
}
|
|
4778
4915
|
}
|
|
@@ -5051,15 +5188,20 @@ ${colors_default.bold("How it works:")}
|
|
|
5051
5188
|
5. Frameworks that ignore PORT (Vite, VitePlus, Astro, React Router, Angular,
|
|
5052
5189
|
Expo, React Native) get --port and, when needed, --host flags
|
|
5053
5190
|
injected automatically
|
|
5191
|
+
6. The proxy listens only on 127.0.0.1 and ::1 unless LAN mode is enabled
|
|
5192
|
+
Elevated proxy processes keep the invoking user's ~/.portless state directory.
|
|
5054
5193
|
|
|
5055
5194
|
${colors_default.bold("HTTP/2 + HTTPS (default):")}
|
|
5056
5195
|
HTTPS with HTTP/2 multiplexing is enabled by default (faster page loads).
|
|
5057
5196
|
On first use, portless generates a local CA and adds it to your
|
|
5058
5197
|
system trust store. No browser warnings. Disable with --no-tls.
|
|
5198
|
+
On WSL, portless also adds the CA to the Windows user certificate store.
|
|
5059
5199
|
|
|
5060
5200
|
${colors_default.bold("LAN mode:")}
|
|
5061
5201
|
Use --lan to make services accessible from other devices (phones,
|
|
5062
5202
|
tablets) on the same WiFi network via mDNS (.local domains).
|
|
5203
|
+
Normal mode binds only to 127.0.0.1 and ::1.
|
|
5204
|
+
LAN mode binds to 0.0.0.0 and ::.
|
|
5063
5205
|
Useful for testing React Native / Expo apps on real devices.
|
|
5064
5206
|
Expo keeps Metro's default LAN host behavior in this mode.
|
|
5065
5207
|
Auto-detected LAN IPs follow network changes automatically.
|
|
@@ -5157,13 +5299,13 @@ ${colors_default.bold("Reserved names:")}
|
|
|
5157
5299
|
process.exit(0);
|
|
5158
5300
|
}
|
|
5159
5301
|
function printVersion() {
|
|
5160
|
-
console.log("0.15.
|
|
5302
|
+
console.log("0.15.4");
|
|
5161
5303
|
process.exit(0);
|
|
5162
5304
|
}
|
|
5163
5305
|
async function handleTrust() {
|
|
5164
5306
|
const { dir } = await discoverState();
|
|
5165
|
-
if (!
|
|
5166
|
-
|
|
5307
|
+
if (!fs10.existsSync(dir)) {
|
|
5308
|
+
fs10.mkdirSync(dir, { recursive: true });
|
|
5167
5309
|
}
|
|
5168
5310
|
const { caGenerated } = ensureCerts(dir);
|
|
5169
5311
|
if (caGenerated) {
|
|
@@ -5211,7 +5353,8 @@ directory, and PORTLESS_STATE_DIR when set), and removes the portless block
|
|
|
5211
5353
|
from ${HOSTS_DISPLAY}.
|
|
5212
5354
|
|
|
5213
5355
|
Only allowlisted filenames under each state directory are deleted. Custom
|
|
5214
|
-
certificate paths from --cert and --key are never removed.
|
|
5356
|
+
certificate paths from --cert and --key are never removed. If trust removal
|
|
5357
|
+
fails, the CA certificate and key are retained so clean can safely retry.
|
|
5215
5358
|
|
|
5216
5359
|
macOS/Linux may prompt for sudo when the proxy, trust store, or ${HOSTS_DISPLAY}
|
|
5217
5360
|
require elevated privileges. On Windows, run as Administrator if needed.
|
|
@@ -5267,27 +5410,32 @@ ${colors_default.bold("Options:")}
|
|
|
5267
5410
|
}
|
|
5268
5411
|
}
|
|
5269
5412
|
const stateDirs = collectStateDirsForCleanup();
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
const wasTrusted = isCATrusted(stateDir);
|
|
5274
|
-
if (!wasTrusted) continue;
|
|
5275
|
-
const untrustResult = untrustCA(stateDir);
|
|
5413
|
+
const failedCAStateDirs = /* @__PURE__ */ new Set();
|
|
5414
|
+
const caRemovalResults = attemptCATrustRemovalForCleanup(stateDirs, untrustCA);
|
|
5415
|
+
for (const [stateDir, untrustResult] of caRemovalResults) {
|
|
5276
5416
|
if (untrustResult.removed) {
|
|
5277
5417
|
console.log(colors_default.green("Removed local CA from the system trust store."));
|
|
5278
|
-
} else
|
|
5418
|
+
} else {
|
|
5419
|
+
failedCAStateDirs.add(stateDir);
|
|
5279
5420
|
console.warn(
|
|
5280
5421
|
colors_default.yellow(
|
|
5281
|
-
`Could not remove CA from trust store: ${untrustResult.error}
|
|
5422
|
+
`Could not remove CA from trust store: ${untrustResult.error ?? "unknown error"}
|
|
5282
5423
|
Try: sudo portless clean (Linux), or delete the certificate manually.`
|
|
5283
5424
|
)
|
|
5284
5425
|
);
|
|
5285
5426
|
}
|
|
5286
5427
|
}
|
|
5287
5428
|
for (const stateDir of stateDirs) {
|
|
5288
|
-
removePortlessStateFiles(stateDir
|
|
5429
|
+
removePortlessStateFiles(stateDir, {
|
|
5430
|
+
preserveCAIdentity: failedCAStateDirs.has(stateDir)
|
|
5431
|
+
});
|
|
5289
5432
|
}
|
|
5290
5433
|
console.log(colors_default.green("Removed portless state files from known state directories."));
|
|
5434
|
+
if (failedCAStateDirs.size > 0) {
|
|
5435
|
+
console.warn(
|
|
5436
|
+
colors_default.yellow("Retained CA identity files so trust removal can be retried safely.")
|
|
5437
|
+
);
|
|
5438
|
+
}
|
|
5291
5439
|
if (cleanHostsFile()) {
|
|
5292
5440
|
console.log(colors_default.green(`Removed portless entries from ${HOSTS_DISPLAY}.`));
|
|
5293
5441
|
} else if (!isWindows && process.getuid?.() !== 0) {
|
|
@@ -5613,7 +5761,7 @@ function isProcessAliveForDoctor(pid) {
|
|
|
5613
5761
|
}
|
|
5614
5762
|
function checkPathWritable(targetPath) {
|
|
5615
5763
|
try {
|
|
5616
|
-
|
|
5764
|
+
fs10.accessSync(targetPath, fs10.constants.W_OK);
|
|
5617
5765
|
return true;
|
|
5618
5766
|
} catch {
|
|
5619
5767
|
return false;
|
|
@@ -5622,7 +5770,7 @@ function checkPathWritable(targetPath) {
|
|
|
5622
5770
|
function findExistingAncestor(targetPath) {
|
|
5623
5771
|
let current = targetPath;
|
|
5624
5772
|
for (; ; ) {
|
|
5625
|
-
if (
|
|
5773
|
+
if (fs10.existsSync(current)) return current;
|
|
5626
5774
|
const parent = path9.dirname(current);
|
|
5627
5775
|
if (parent === current) return null;
|
|
5628
5776
|
current = parent;
|
|
@@ -5693,7 +5841,7 @@ ${colors_default.bold("Options:")}
|
|
|
5693
5841
|
const store = new RouteStore(state.dir, {
|
|
5694
5842
|
onWarning: (msg) => add("warn", msg)
|
|
5695
5843
|
});
|
|
5696
|
-
const hasPortFile =
|
|
5844
|
+
const hasPortFile = fs10.existsSync(store.portFilePath);
|
|
5697
5845
|
const configuredTls = hasPortFile ? state.tls : !isHttpsEnvDisabled();
|
|
5698
5846
|
const configuredPort = hasPortFile ? state.port : getDefaultPort(configuredTls);
|
|
5699
5847
|
const initialProxyRunning = await isProxyRunning(state.port, state.tls);
|
|
@@ -5705,9 +5853,9 @@ ${colors_default.bold("Options:")}
|
|
|
5705
5853
|
const proxyTls = proxyRunning || portListening || hasPortFile ? probeTls : configuredTls;
|
|
5706
5854
|
const currentProxyStateIsHttp = (proxyRunning || portListening || hasPortFile) && !proxyTls;
|
|
5707
5855
|
const proxyUsesCustomCert = proxyTls && readCustomCertMarker(state.dir);
|
|
5708
|
-
const stateExists =
|
|
5856
|
+
const stateExists = fs10.existsSync(state.dir);
|
|
5709
5857
|
console.log(colors_default.blue.bold("\nportless doctor\n"));
|
|
5710
|
-
console.log(`Version: ${"0.15.
|
|
5858
|
+
console.log(`Version: ${"0.15.4"}`);
|
|
5711
5859
|
console.log(`Node.js: ${process.versions.node}`);
|
|
5712
5860
|
console.log(`Platform: ${process.platform} ${process.arch}`);
|
|
5713
5861
|
console.log(`State dir: ${state.dir}`);
|
|
@@ -5724,7 +5872,7 @@ ${colors_default.bold("Options:")}
|
|
|
5724
5872
|
}
|
|
5725
5873
|
if (stateExists) {
|
|
5726
5874
|
try {
|
|
5727
|
-
const stat =
|
|
5875
|
+
const stat = fs10.statSync(state.dir);
|
|
5728
5876
|
if (!stat.isDirectory()) {
|
|
5729
5877
|
add("fail", `State path exists but is not a directory: ${state.dir}`);
|
|
5730
5878
|
} else if (checkPathWritable(state.dir)) {
|
|
@@ -5744,7 +5892,7 @@ ${colors_default.bold("Options:")}
|
|
|
5744
5892
|
`State directory does not exist and no writable ancestor was found: ${state.dir}`
|
|
5745
5893
|
);
|
|
5746
5894
|
} else {
|
|
5747
|
-
const ancestorStat =
|
|
5895
|
+
const ancestorStat = fs10.statSync(ancestor);
|
|
5748
5896
|
if (!ancestorStat.isDirectory()) {
|
|
5749
5897
|
add("fail", `State directory does not exist and ancestor is not a directory: ${ancestor}`);
|
|
5750
5898
|
} else if (checkPathWritable(ancestor)) {
|
|
@@ -5770,9 +5918,9 @@ ${colors_default.bold("Options:")}
|
|
|
5770
5918
|
doctorProxyStartHint(proxyPort, proxyTls)
|
|
5771
5919
|
);
|
|
5772
5920
|
}
|
|
5773
|
-
if (
|
|
5921
|
+
if (fs10.existsSync(store.pidPath)) {
|
|
5774
5922
|
try {
|
|
5775
|
-
const rawPid =
|
|
5923
|
+
const rawPid = fs10.readFileSync(store.pidPath, "utf-8").trim();
|
|
5776
5924
|
const pid = parseInt(rawPid, 10);
|
|
5777
5925
|
if (isNaN(pid) || pid <= 0) {
|
|
5778
5926
|
add("fail", `Proxy PID file is invalid: ${store.pidPath}`);
|
|
@@ -5822,7 +5970,7 @@ ${colors_default.bold("Options:")}
|
|
|
5822
5970
|
add("info", "Generated local CA is not required for custom TLS certificates.");
|
|
5823
5971
|
} else if (proxyTls) {
|
|
5824
5972
|
const caPath = path9.join(state.dir, "ca.pem");
|
|
5825
|
-
if (
|
|
5973
|
+
if (fs10.existsSync(caPath)) {
|
|
5826
5974
|
if (isCATrusted(state.dir)) {
|
|
5827
5975
|
add("ok", "Local CA is trusted by the OS trust store.");
|
|
5828
5976
|
} else {
|
|
@@ -5991,6 +6139,8 @@ ${colors_default.bold("Usage:")}
|
|
|
5991
6139
|
${colors_default.cyan("portless proxy stop")} Stop the proxy
|
|
5992
6140
|
|
|
5993
6141
|
${colors_default.bold("LAN mode (--lan):")}
|
|
6142
|
+
Without LAN mode, the proxy listens only on 127.0.0.1 and ::1.
|
|
6143
|
+
LAN mode explicitly binds the proxy to 0.0.0.0 and ::.
|
|
5994
6144
|
Makes services accessible from other devices on the same WiFi network
|
|
5995
6145
|
via mDNS (.local domains). Useful for testing on real mobile devices.
|
|
5996
6146
|
Auto-detects your LAN IP and follows changes automatically, or use
|
|
@@ -6247,7 +6397,7 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
6247
6397
|
} else {
|
|
6248
6398
|
console.error(colors_default.red("Proxy process started but is not responding."));
|
|
6249
6399
|
const logPath2 = path9.join(resolveStateDir(proxyPort), "proxy.log");
|
|
6250
|
-
if (
|
|
6400
|
+
if (fs10.existsSync(logPath2)) {
|
|
6251
6401
|
console.error(colors_default.gray(`Logs: ${logPath2}`));
|
|
6252
6402
|
}
|
|
6253
6403
|
}
|
|
@@ -6286,8 +6436,8 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
6286
6436
|
store.ensureDir();
|
|
6287
6437
|
if (customCertPath && customKeyPath) {
|
|
6288
6438
|
try {
|
|
6289
|
-
const cert =
|
|
6290
|
-
const key =
|
|
6439
|
+
const cert = fs10.readFileSync(customCertPath);
|
|
6440
|
+
const key = fs10.readFileSync(customKeyPath);
|
|
6291
6441
|
const certStr = cert.toString("utf-8");
|
|
6292
6442
|
const keyStr = key.toString("utf-8");
|
|
6293
6443
|
if (!certStr.includes("-----BEGIN CERTIFICATE-----")) {
|
|
@@ -6332,9 +6482,9 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
6332
6482
|
console.warn(colors_default.cyan(" portless trust"));
|
|
6333
6483
|
}
|
|
6334
6484
|
}
|
|
6335
|
-
const cert =
|
|
6336
|
-
const key =
|
|
6337
|
-
const ca =
|
|
6485
|
+
const cert = fs10.readFileSync(certs.certPath);
|
|
6486
|
+
const key = fs10.readFileSync(certs.keyPath);
|
|
6487
|
+
const ca = fs10.readFileSync(certs.caPath);
|
|
6338
6488
|
tlsOptions = {
|
|
6339
6489
|
cert,
|
|
6340
6490
|
key,
|
|
@@ -6359,10 +6509,10 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
6359
6509
|
}
|
|
6360
6510
|
store.ensureDir();
|
|
6361
6511
|
const logPath = path9.join(stateDir, "proxy.log");
|
|
6362
|
-
const logFd =
|
|
6512
|
+
const logFd = fs10.openSync(logPath, "a");
|
|
6363
6513
|
try {
|
|
6364
6514
|
try {
|
|
6365
|
-
|
|
6515
|
+
fs10.chmodSync(logPath, FILE_MODE);
|
|
6366
6516
|
} catch {
|
|
6367
6517
|
}
|
|
6368
6518
|
fixOwnership(logPath);
|
|
@@ -6394,13 +6544,13 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
6394
6544
|
});
|
|
6395
6545
|
child.unref();
|
|
6396
6546
|
} finally {
|
|
6397
|
-
|
|
6547
|
+
fs10.closeSync(logFd);
|
|
6398
6548
|
}
|
|
6399
6549
|
if (!await waitForProxy(proxyPort, void 0, void 0, useHttps)) {
|
|
6400
6550
|
console.error(colors_default.red("Proxy failed to start (timed out waiting for it to listen)."));
|
|
6401
6551
|
console.error(colors_default.blue("Try starting the proxy in the foreground to see the error:"));
|
|
6402
6552
|
console.error(colors_default.cyan(" portless proxy start --foreground"));
|
|
6403
|
-
if (
|
|
6553
|
+
if (fs10.existsSync(logPath)) {
|
|
6404
6554
|
console.error(colors_default.gray(`Logs: ${logPath}`));
|
|
6405
6555
|
}
|
|
6406
6556
|
process.exit(1);
|
|
@@ -6553,7 +6703,7 @@ async function spawnProxiedApp(app, stateDir, proxyPort, tls2, tlds, exitCodes)
|
|
|
6553
6703
|
};
|
|
6554
6704
|
if (tls2) {
|
|
6555
6705
|
const caPath = path9.join(stateDir, "ca.pem");
|
|
6556
|
-
if (
|
|
6706
|
+
if (fs10.existsSync(caPath)) {
|
|
6557
6707
|
env.NODE_EXTRA_CA_CERTS = caPath;
|
|
6558
6708
|
}
|
|
6559
6709
|
}
|
|
@@ -6743,7 +6893,7 @@ async function runWithTurbo(wsRoot, stateDir, proxyPort, tls2, tlds, scriptName,
|
|
|
6743
6893
|
};
|
|
6744
6894
|
if (tls2) {
|
|
6745
6895
|
const caPath = path9.join(stateDir, "ca.pem");
|
|
6746
|
-
if (
|
|
6896
|
+
if (fs10.existsSync(caPath)) {
|
|
6747
6897
|
entry.NODE_EXTRA_CA_CERTS = caPath;
|
|
6748
6898
|
}
|
|
6749
6899
|
}
|