portless 0.9.6 → 0.10.1
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 +49 -7
- package/dist/{chunk-OPTRASOS.js → chunk-FM7IA4AF.js} +139 -14
- package/dist/cli.js +1063 -151
- package/dist/index.d.ts +6 -1
- package/dist/index.js +3 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3,12 +3,17 @@ import {
|
|
|
3
3
|
DEFAULT_TLD,
|
|
4
4
|
FALLBACK_PROXY_PORT,
|
|
5
5
|
FILE_MODE,
|
|
6
|
+
INTERNAL_LAN_IP_ENV,
|
|
7
|
+
INTERNAL_LAN_IP_FLAG,
|
|
6
8
|
PRIVILEGED_PORT_THRESHOLD,
|
|
7
9
|
RISKY_TLDS,
|
|
8
10
|
RouteConflictError,
|
|
9
11
|
RouteStore,
|
|
12
|
+
SYSTEM_STATE_DIR,
|
|
13
|
+
USER_STATE_DIR,
|
|
10
14
|
WAIT_FOR_PROXY_INTERVAL_MS,
|
|
11
15
|
WAIT_FOR_PROXY_MAX_ATTEMPTS,
|
|
16
|
+
buildProxyStartConfig,
|
|
12
17
|
cleanHostsFile,
|
|
13
18
|
createHttpRedirectServer,
|
|
14
19
|
createProxyServer,
|
|
@@ -19,23 +24,29 @@ import {
|
|
|
19
24
|
formatUrl,
|
|
20
25
|
getDefaultPort,
|
|
21
26
|
getDefaultTld,
|
|
22
|
-
getProtocolPort,
|
|
23
27
|
injectFrameworkFlags,
|
|
24
28
|
isErrnoException,
|
|
25
29
|
isHttpsEnvDisabled,
|
|
30
|
+
isLanEnvEnabled,
|
|
31
|
+
isPortListening,
|
|
26
32
|
isProxyRunning,
|
|
27
33
|
isWildcardEnvEnabled,
|
|
28
34
|
isWindows,
|
|
29
35
|
parseHostname,
|
|
30
36
|
prompt,
|
|
37
|
+
readLanMarker,
|
|
38
|
+
readTldFromDir,
|
|
39
|
+
readTlsMarker,
|
|
31
40
|
resolveStateDir,
|
|
41
|
+
shouldAutoSyncHosts,
|
|
32
42
|
spawnCommand,
|
|
33
43
|
syncHostsFile,
|
|
34
44
|
validateTld,
|
|
35
45
|
waitForProxy,
|
|
46
|
+
writeLanMarker,
|
|
36
47
|
writeTldFile,
|
|
37
48
|
writeTlsMarker
|
|
38
|
-
} from "./chunk-
|
|
49
|
+
} from "./chunk-FM7IA4AF.js";
|
|
39
50
|
|
|
40
51
|
// src/colors.ts
|
|
41
52
|
function supportsColor() {
|
|
@@ -63,9 +74,9 @@ var gray = wrap("90", "39");
|
|
|
63
74
|
var colors_default = { bold, red, green, yellow, blue, cyan, white, gray };
|
|
64
75
|
|
|
65
76
|
// src/cli.ts
|
|
66
|
-
import * as
|
|
67
|
-
import * as
|
|
68
|
-
import { spawn, spawnSync } from "child_process";
|
|
77
|
+
import * as fs4 from "fs";
|
|
78
|
+
import * as path4 from "path";
|
|
79
|
+
import { spawn as spawn2, spawnSync as spawnSync2 } from "child_process";
|
|
69
80
|
|
|
70
81
|
// src/certs.ts
|
|
71
82
|
import * as fs from "fs";
|
|
@@ -135,6 +146,14 @@ function isCertValid(certPath) {
|
|
|
135
146
|
return false;
|
|
136
147
|
}
|
|
137
148
|
}
|
|
149
|
+
function isCertSansComplete(certPath) {
|
|
150
|
+
try {
|
|
151
|
+
const text = openssl(["x509", "-in", certPath, "-noout", "-text"]);
|
|
152
|
+
return /DNS:\*\.local\b/.test(text);
|
|
153
|
+
} catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
138
157
|
function isCertSignatureStrong(certPath) {
|
|
139
158
|
try {
|
|
140
159
|
const text = openssl(["x509", "-in", certPath, "-noout", "-text"]);
|
|
@@ -216,6 +235,7 @@ function generateServerCert(stateDir) {
|
|
|
216
235
|
const extPath = path.join(stateDir, "server-ext.cnf");
|
|
217
236
|
openssl(["ecparam", "-genkey", "-name", "prime256v1", "-noout", "-out", serverKeyPath]);
|
|
218
237
|
openssl(["req", "-new", "-key", serverKeyPath, "-out", csrPath, "-subj", "/CN=localhost"]);
|
|
238
|
+
const sans = ["DNS:localhost", "DNS:*.localhost", "DNS:*.local"];
|
|
219
239
|
fs.writeFileSync(
|
|
220
240
|
extPath,
|
|
221
241
|
[
|
|
@@ -223,7 +243,7 @@ function generateServerCert(stateDir) {
|
|
|
223
243
|
"basicConstraints=CA:FALSE",
|
|
224
244
|
"keyUsage=digitalSignature,keyEncipherment",
|
|
225
245
|
"extendedKeyUsage=serverAuth",
|
|
226
|
-
"
|
|
246
|
+
`subjectAltName=${sans.join(",")}`
|
|
227
247
|
].join("\n") + "\n"
|
|
228
248
|
);
|
|
229
249
|
const srlPath = path.join(stateDir, "ca.srl");
|
|
@@ -273,7 +293,7 @@ function ensureCerts(stateDir) {
|
|
|
273
293
|
generateCA(stateDir);
|
|
274
294
|
caGenerated = true;
|
|
275
295
|
}
|
|
276
|
-
if (caGenerated || !fileExists(serverCertPath) || !fileExists(serverKeyPath) || !isCertValid(serverCertPath) || !isCertSignatureStrong(serverCertPath)) {
|
|
296
|
+
if (caGenerated || !fileExists(serverCertPath) || !fileExists(serverKeyPath) || !isCertValid(serverCertPath) || !isCertSignatureStrong(serverCertPath) || !isCertSansComplete(serverCertPath)) {
|
|
277
297
|
generateServerCert(stateDir);
|
|
278
298
|
}
|
|
279
299
|
return {
|
|
@@ -591,6 +611,129 @@ function trustCA(stateDir) {
|
|
|
591
611
|
return { trusted: false, error: message };
|
|
592
612
|
}
|
|
593
613
|
}
|
|
614
|
+
function untrustCA(stateDir) {
|
|
615
|
+
const caCertPath = path.join(stateDir, CA_CERT_FILE);
|
|
616
|
+
if (!fileExists(caCertPath)) {
|
|
617
|
+
return { removed: true };
|
|
618
|
+
}
|
|
619
|
+
if (!isCATrusted(stateDir)) {
|
|
620
|
+
return { removed: true };
|
|
621
|
+
}
|
|
622
|
+
try {
|
|
623
|
+
if (process.platform === "darwin") {
|
|
624
|
+
return untrustCAMacOS(caCertPath);
|
|
625
|
+
}
|
|
626
|
+
if (process.platform === "linux") {
|
|
627
|
+
return untrustCALinux(stateDir);
|
|
628
|
+
}
|
|
629
|
+
if (process.platform === "win32") {
|
|
630
|
+
return untrustCAWindows(caCertPath);
|
|
631
|
+
}
|
|
632
|
+
return { removed: false, error: `Unsupported platform: ${process.platform}` };
|
|
633
|
+
} catch (err) {
|
|
634
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
635
|
+
return { removed: false, error: message };
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
function untrustCAMacOS(caCertPath) {
|
|
639
|
+
const errors = [];
|
|
640
|
+
const tryExec = (args) => {
|
|
641
|
+
try {
|
|
642
|
+
execFileSync("security", args, { stdio: "pipe", timeout: 3e4 });
|
|
643
|
+
return true;
|
|
644
|
+
} catch (err) {
|
|
645
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
646
|
+
errors.push(message);
|
|
647
|
+
return false;
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
if (tryExec(["remove-trusted-cert", caCertPath])) {
|
|
651
|
+
return isCATrustedMacOSAfterAttempt(caCertPath) ? { removed: false, error: errors.join("; ") || "Trust entry may still be present" } : { removed: true };
|
|
652
|
+
}
|
|
653
|
+
const login = loginKeychainPath();
|
|
654
|
+
tryExec(["delete-certificate", "-c", CA_COMMON_NAME, login]);
|
|
655
|
+
tryExec(["delete-certificate", "-c", CA_COMMON_NAME, "/Library/Keychains/System.keychain"]);
|
|
656
|
+
return isCATrustedMacOSAfterAttempt(caCertPath) ? { removed: false, error: errors.join("; ") || "Could not remove CA from keychain (try sudo)" } : { removed: true };
|
|
657
|
+
}
|
|
658
|
+
function isCATrustedMacOSAfterAttempt(caCertPath) {
|
|
659
|
+
try {
|
|
660
|
+
const isRoot = (process.getuid?.() ?? -1) === 0;
|
|
661
|
+
const sudoUser = process.env.SUDO_USER;
|
|
662
|
+
if (isRoot && sudoUser) {
|
|
663
|
+
execFileSync(
|
|
664
|
+
"sudo",
|
|
665
|
+
["-u", sudoUser, "security", "verify-cert", "-c", caCertPath, "-L", "-p", "ssl"],
|
|
666
|
+
{ stdio: "pipe", timeout: 5e3 }
|
|
667
|
+
);
|
|
668
|
+
} else {
|
|
669
|
+
execFileSync("security", ["verify-cert", "-c", caCertPath, "-L", "-p", "ssl"], {
|
|
670
|
+
stdio: "pipe",
|
|
671
|
+
timeout: 5e3
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
return true;
|
|
675
|
+
} catch {
|
|
676
|
+
return false;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
function untrustCALinux(stateDir) {
|
|
680
|
+
const errors = [];
|
|
681
|
+
let deletedAny = false;
|
|
682
|
+
for (const config of Object.values(LINUX_CA_TRUST_CONFIGS)) {
|
|
683
|
+
const dest = path.join(config.certDir, "portless-ca.crt");
|
|
684
|
+
try {
|
|
685
|
+
if (fileExists(dest)) {
|
|
686
|
+
const ours = fs.readFileSync(path.join(stateDir, CA_CERT_FILE), "utf-8").trim();
|
|
687
|
+
const installed = fs.readFileSync(dest, "utf-8").trim();
|
|
688
|
+
if (ours === installed) {
|
|
689
|
+
fs.unlinkSync(dest);
|
|
690
|
+
deletedAny = true;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
} catch (err) {
|
|
694
|
+
errors.push(err instanceof Error ? err.message : String(err));
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
if (deletedAny) {
|
|
698
|
+
try {
|
|
699
|
+
const config = getLinuxCATrustConfig();
|
|
700
|
+
execFileSync(config.updateCommand, [], { stdio: "pipe", timeout: 3e4 });
|
|
701
|
+
} catch (err) {
|
|
702
|
+
errors.push(err instanceof Error ? err.message : String(err));
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (isCATrusted(stateDir)) {
|
|
706
|
+
return {
|
|
707
|
+
removed: false,
|
|
708
|
+
error: errors.join("; ") || "CA still trusted (remove portless-ca.crt and run the distro CA update command, often with sudo)"
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
return { removed: true };
|
|
712
|
+
}
|
|
713
|
+
function untrustCAWindows(caCertPath) {
|
|
714
|
+
try {
|
|
715
|
+
const fingerprint = openssl(["x509", "-in", caCertPath, "-noout", "-fingerprint", "-sha1"]).trim().replace(/^.*=/, "").replace(/:/g, "").toLowerCase();
|
|
716
|
+
const storeListing = execFileSync("certutil", ["-store", "-user", "Root"], {
|
|
717
|
+
encoding: "utf-8",
|
|
718
|
+
timeout: 1e4,
|
|
719
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
720
|
+
});
|
|
721
|
+
const normalized = storeListing.replace(/\s/g, "").toLowerCase();
|
|
722
|
+
if (!normalized.includes(fingerprint)) {
|
|
723
|
+
return { removed: true };
|
|
724
|
+
}
|
|
725
|
+
execFileSync("certutil", ["-delstore", "-user", "Root", "portless Local CA"], {
|
|
726
|
+
stdio: "pipe",
|
|
727
|
+
timeout: 3e4
|
|
728
|
+
});
|
|
729
|
+
if (isCATrustedWindows(caCertPath)) {
|
|
730
|
+
return { removed: false, error: "certutil could not remove the portless CA from Root" };
|
|
731
|
+
}
|
|
732
|
+
return { removed: true };
|
|
733
|
+
} catch (err) {
|
|
734
|
+
return { removed: false, error: err instanceof Error ? err.message : String(err) };
|
|
735
|
+
}
|
|
736
|
+
}
|
|
594
737
|
|
|
595
738
|
// src/auto.ts
|
|
596
739
|
import { createHash } from "crypto";
|
|
@@ -766,12 +909,398 @@ function readBranchFromHead(gitdir) {
|
|
|
766
909
|
}
|
|
767
910
|
}
|
|
768
911
|
|
|
912
|
+
// src/clean-utils.ts
|
|
913
|
+
import * as fs3 from "fs";
|
|
914
|
+
import * as path3 from "path";
|
|
915
|
+
var PORTLESS_STATE_FILES = [
|
|
916
|
+
"routes.json",
|
|
917
|
+
"routes.lock",
|
|
918
|
+
"proxy.pid",
|
|
919
|
+
"proxy.port",
|
|
920
|
+
"proxy.log",
|
|
921
|
+
"proxy.tls",
|
|
922
|
+
"proxy.tld",
|
|
923
|
+
"proxy.lan",
|
|
924
|
+
"ca-key.pem",
|
|
925
|
+
"ca.pem",
|
|
926
|
+
"server-key.pem",
|
|
927
|
+
"server.pem",
|
|
928
|
+
"server.csr",
|
|
929
|
+
"server-ext.cnf",
|
|
930
|
+
"ca.srl"
|
|
931
|
+
];
|
|
932
|
+
var HOST_CERTS_DIR2 = "host-certs";
|
|
933
|
+
function collectStateDirsForCleanup() {
|
|
934
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
935
|
+
const add = (d) => {
|
|
936
|
+
const trimmed = d?.trim();
|
|
937
|
+
if (!trimmed) return;
|
|
938
|
+
const resolved = path3.resolve(trimmed);
|
|
939
|
+
if (fs3.existsSync(resolved)) dirs.add(resolved);
|
|
940
|
+
};
|
|
941
|
+
add(USER_STATE_DIR);
|
|
942
|
+
add(SYSTEM_STATE_DIR);
|
|
943
|
+
add(process.env.PORTLESS_STATE_DIR);
|
|
944
|
+
return [...dirs];
|
|
945
|
+
}
|
|
946
|
+
function removePortlessStateFiles(dir) {
|
|
947
|
+
for (const f of PORTLESS_STATE_FILES) {
|
|
948
|
+
try {
|
|
949
|
+
fs3.unlinkSync(path3.join(dir, f));
|
|
950
|
+
} catch {
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
try {
|
|
954
|
+
fs3.rmSync(path3.join(dir, HOST_CERTS_DIR2), { recursive: true, force: true });
|
|
955
|
+
} catch {
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// src/mdns.ts
|
|
960
|
+
import { spawn, spawnSync } from "child_process";
|
|
961
|
+
|
|
962
|
+
// src/lan-ip.ts
|
|
963
|
+
import { createSocket } from "dgram";
|
|
964
|
+
import { networkInterfaces } from "os";
|
|
965
|
+
var PROBE_HOST = "1.1.1.1";
|
|
966
|
+
var PROBE_PORT = 53;
|
|
967
|
+
var NO_ROUTE_IP = "0.0.0.0";
|
|
968
|
+
function isIPv4Family(family) {
|
|
969
|
+
return family === "IPv4" || family === 4;
|
|
970
|
+
}
|
|
971
|
+
function parseMac(macStr) {
|
|
972
|
+
return macStr.split(":").slice(0, 16).map((seq) => parseInt(seq, 16));
|
|
973
|
+
}
|
|
974
|
+
function isInternalInterface(iname, macStr, internal) {
|
|
975
|
+
if (internal) {
|
|
976
|
+
return true;
|
|
977
|
+
}
|
|
978
|
+
const mac = parseMac(macStr);
|
|
979
|
+
if (mac.every((x) => !x)) {
|
|
980
|
+
return true;
|
|
981
|
+
}
|
|
982
|
+
if (mac[0] === 0 && mac[1] === 21 && mac[2] === 93) {
|
|
983
|
+
return true;
|
|
984
|
+
}
|
|
985
|
+
if (iname.includes("vEthernet") || /^bridge\d+$/.test(iname)) {
|
|
986
|
+
return true;
|
|
987
|
+
}
|
|
988
|
+
return false;
|
|
989
|
+
}
|
|
990
|
+
function probeDefaultRouteIPv4() {
|
|
991
|
+
return new Promise((resolve3, reject) => {
|
|
992
|
+
const socket = createSocket({ type: "udp4", reuseAddr: true });
|
|
993
|
+
socket.on("error", (error) => {
|
|
994
|
+
socket.close();
|
|
995
|
+
socket.unref();
|
|
996
|
+
reject(error);
|
|
997
|
+
});
|
|
998
|
+
socket.connect(PROBE_PORT, PROBE_HOST, () => {
|
|
999
|
+
const addr = socket.address();
|
|
1000
|
+
socket.close();
|
|
1001
|
+
socket.unref();
|
|
1002
|
+
if (addr && "address" in addr && addr.address && addr.address !== NO_ROUTE_IP) {
|
|
1003
|
+
resolve3(addr.address);
|
|
1004
|
+
} else {
|
|
1005
|
+
reject(new Error("No route to host"));
|
|
1006
|
+
}
|
|
1007
|
+
});
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
function findInterfaceRowForIp(ip) {
|
|
1011
|
+
const ifs = networkInterfaces();
|
|
1012
|
+
for (const iname of Object.keys(ifs)) {
|
|
1013
|
+
const entries = ifs[iname];
|
|
1014
|
+
if (!entries) continue;
|
|
1015
|
+
for (const e of entries) {
|
|
1016
|
+
if (!isIPv4Family(e.family)) continue;
|
|
1017
|
+
if (e.address !== ip) continue;
|
|
1018
|
+
return { iname, address: e.address, mac: e.mac, internal: e.internal };
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
return null;
|
|
1022
|
+
}
|
|
1023
|
+
async function getLocalNetworkIp() {
|
|
1024
|
+
try {
|
|
1025
|
+
const ip = await probeDefaultRouteIPv4();
|
|
1026
|
+
if (ip === "127.0.0.1") {
|
|
1027
|
+
return null;
|
|
1028
|
+
}
|
|
1029
|
+
const row = findInterfaceRowForIp(ip);
|
|
1030
|
+
if (!row) {
|
|
1031
|
+
return null;
|
|
1032
|
+
}
|
|
1033
|
+
if (row.address === "127.0.0.1") {
|
|
1034
|
+
return null;
|
|
1035
|
+
}
|
|
1036
|
+
if (isInternalInterface(row.iname, row.mac, row.internal)) {
|
|
1037
|
+
return null;
|
|
1038
|
+
}
|
|
1039
|
+
return row.address;
|
|
1040
|
+
} catch {
|
|
1041
|
+
return null;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// src/mdns.ts
|
|
1046
|
+
var activePublishers = /* @__PURE__ */ new Map();
|
|
1047
|
+
var LAN_IP_POLL_INTERVAL_MS = 5e3;
|
|
1048
|
+
function getMdnsPublisher() {
|
|
1049
|
+
if (process.platform === "darwin") {
|
|
1050
|
+
return {
|
|
1051
|
+
command: "dns-sd",
|
|
1052
|
+
probeArgs: ["-h"],
|
|
1053
|
+
missingReason: "dns-sd not found",
|
|
1054
|
+
buildArgs: (fqdn, name, port, ip) => [
|
|
1055
|
+
"-P",
|
|
1056
|
+
name,
|
|
1057
|
+
"_http._tcp",
|
|
1058
|
+
"local",
|
|
1059
|
+
port.toString(),
|
|
1060
|
+
fqdn,
|
|
1061
|
+
ip
|
|
1062
|
+
]
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
if (process.platform === "linux") {
|
|
1066
|
+
return {
|
|
1067
|
+
command: "avahi-publish-address",
|
|
1068
|
+
probeArgs: ["--help"],
|
|
1069
|
+
missingReason: "avahi-publish-address not found. Install avahi-utils: sudo apt install avahi-utils",
|
|
1070
|
+
buildArgs: (fqdn, _name, _port, ip) => ["-R", fqdn, ip]
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
return null;
|
|
1074
|
+
}
|
|
1075
|
+
function hasCommand(command, probeArgs) {
|
|
1076
|
+
const result = spawnSync(command, probeArgs, {
|
|
1077
|
+
stdio: "ignore",
|
|
1078
|
+
timeout: 1e3,
|
|
1079
|
+
windowsHide: true
|
|
1080
|
+
});
|
|
1081
|
+
return result.error?.code !== "ENOENT";
|
|
1082
|
+
}
|
|
1083
|
+
function startLanIpMonitor(options) {
|
|
1084
|
+
const resolveIp = options.resolveIp ?? getLocalNetworkIp;
|
|
1085
|
+
let currentIp = options.initialIp;
|
|
1086
|
+
let stopped = false;
|
|
1087
|
+
let polling = false;
|
|
1088
|
+
const poll = async () => {
|
|
1089
|
+
if (stopped || polling) return;
|
|
1090
|
+
polling = true;
|
|
1091
|
+
try {
|
|
1092
|
+
const nextIp = await resolveIp();
|
|
1093
|
+
if (stopped || nextIp === currentIp) return;
|
|
1094
|
+
const previousIp = currentIp;
|
|
1095
|
+
currentIp = nextIp;
|
|
1096
|
+
options.onChange(nextIp, previousIp);
|
|
1097
|
+
} catch (error) {
|
|
1098
|
+
options.onError?.(error);
|
|
1099
|
+
} finally {
|
|
1100
|
+
polling = false;
|
|
1101
|
+
}
|
|
1102
|
+
};
|
|
1103
|
+
const timer = setInterval(() => {
|
|
1104
|
+
void poll();
|
|
1105
|
+
}, options.intervalMs ?? LAN_IP_POLL_INTERVAL_MS);
|
|
1106
|
+
timer.unref?.();
|
|
1107
|
+
return {
|
|
1108
|
+
stop: () => {
|
|
1109
|
+
stopped = true;
|
|
1110
|
+
clearInterval(timer);
|
|
1111
|
+
}
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
function isMdnsSupported() {
|
|
1115
|
+
const publisher = getMdnsPublisher();
|
|
1116
|
+
if (!publisher) {
|
|
1117
|
+
return { supported: false, reason: "mDNS publishing is not supported on this platform" };
|
|
1118
|
+
}
|
|
1119
|
+
if (!hasCommand(publisher.command, publisher.probeArgs)) {
|
|
1120
|
+
return { supported: false, reason: publisher.missingReason };
|
|
1121
|
+
}
|
|
1122
|
+
return { supported: true };
|
|
1123
|
+
}
|
|
1124
|
+
function serviceName(hostname) {
|
|
1125
|
+
return hostname.replace(/\.local$/, "");
|
|
1126
|
+
}
|
|
1127
|
+
function publish(hostname, port, ip, onError) {
|
|
1128
|
+
if (activePublishers.has(hostname)) return;
|
|
1129
|
+
const fqdn = hostname.endsWith(".local") ? hostname : `${hostname}.local`;
|
|
1130
|
+
const name = serviceName(fqdn);
|
|
1131
|
+
const publisher = getMdnsPublisher();
|
|
1132
|
+
if (!publisher) {
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
const child = spawn(publisher.command, publisher.buildArgs(fqdn, name, port, ip), {
|
|
1136
|
+
stdio: "ignore",
|
|
1137
|
+
detached: false
|
|
1138
|
+
});
|
|
1139
|
+
child.on("error", (err) => {
|
|
1140
|
+
activePublishers.delete(hostname);
|
|
1141
|
+
const msg = err.code === "ENOENT" ? publisher.missingReason : `mDNS publish error for ${hostname}: ${err.message}`;
|
|
1142
|
+
onError?.(msg);
|
|
1143
|
+
});
|
|
1144
|
+
child.on("exit", () => {
|
|
1145
|
+
activePublishers.delete(hostname);
|
|
1146
|
+
});
|
|
1147
|
+
activePublishers.set(hostname, child);
|
|
1148
|
+
}
|
|
1149
|
+
function unpublish(hostname) {
|
|
1150
|
+
const child = activePublishers.get(hostname);
|
|
1151
|
+
if (!child) return;
|
|
1152
|
+
activePublishers.delete(hostname);
|
|
1153
|
+
child.kill("SIGTERM");
|
|
1154
|
+
}
|
|
1155
|
+
function cleanupAll() {
|
|
1156
|
+
for (const child of activePublishers.values()) {
|
|
1157
|
+
child.kill("SIGTERM");
|
|
1158
|
+
}
|
|
1159
|
+
activePublishers.clear();
|
|
1160
|
+
}
|
|
1161
|
+
|
|
769
1162
|
// src/cli.ts
|
|
1163
|
+
var chalk = colors_default;
|
|
770
1164
|
var HOSTS_DISPLAY = isWindows ? "hosts file" : "/etc/hosts";
|
|
771
1165
|
var DEBOUNCE_MS = 100;
|
|
772
1166
|
var POLL_INTERVAL_MS = 3e3;
|
|
773
1167
|
var EXIT_TIMEOUT_MS = 2e3;
|
|
774
1168
|
var SUDO_SPAWN_TIMEOUT_MS = 3e4;
|
|
1169
|
+
function defaultProxyConfig(tld, useHttps, lanMode) {
|
|
1170
|
+
return {
|
|
1171
|
+
useHttps,
|
|
1172
|
+
customCertPath: null,
|
|
1173
|
+
customKeyPath: null,
|
|
1174
|
+
lanMode,
|
|
1175
|
+
lanIp: null,
|
|
1176
|
+
lanIpExplicit: false,
|
|
1177
|
+
tld: lanMode ? "local" : tld,
|
|
1178
|
+
useWildcard: false
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
function resolveProxyConfig(options) {
|
|
1182
|
+
const config = defaultProxyConfig(
|
|
1183
|
+
options.defaultTld,
|
|
1184
|
+
options.useHttps,
|
|
1185
|
+
options.explicit.lanMode ? options.lanMode : options.persistedLanMode
|
|
1186
|
+
);
|
|
1187
|
+
if (options.explicit.useHttps) {
|
|
1188
|
+
config.useHttps = options.useHttps;
|
|
1189
|
+
if (!options.useHttps) {
|
|
1190
|
+
config.customCertPath = null;
|
|
1191
|
+
config.customKeyPath = null;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
if (options.explicit.customCert) {
|
|
1195
|
+
config.useHttps = true;
|
|
1196
|
+
config.customCertPath = options.customCertPath;
|
|
1197
|
+
config.customKeyPath = options.customKeyPath;
|
|
1198
|
+
}
|
|
1199
|
+
if (options.explicit.lanMode) {
|
|
1200
|
+
config.lanMode = options.lanMode;
|
|
1201
|
+
if (!options.lanMode) {
|
|
1202
|
+
config.lanIp = null;
|
|
1203
|
+
config.lanIpExplicit = false;
|
|
1204
|
+
if (!options.explicit.tld) {
|
|
1205
|
+
config.tld = options.defaultTld;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
if (options.explicit.lanIp && options.lanIp) {
|
|
1210
|
+
config.lanMode = true;
|
|
1211
|
+
config.lanIp = options.lanIp;
|
|
1212
|
+
config.lanIpExplicit = true;
|
|
1213
|
+
}
|
|
1214
|
+
if (options.explicit.tld) {
|
|
1215
|
+
config.tld = options.tld;
|
|
1216
|
+
}
|
|
1217
|
+
if (options.explicit.useWildcard) {
|
|
1218
|
+
config.useWildcard = options.useWildcard;
|
|
1219
|
+
}
|
|
1220
|
+
if (!config.lanMode) {
|
|
1221
|
+
config.lanIp = null;
|
|
1222
|
+
config.lanIpExplicit = false;
|
|
1223
|
+
}
|
|
1224
|
+
if (config.lanMode) {
|
|
1225
|
+
config.tld = "local";
|
|
1226
|
+
if (!config.lanIpExplicit) {
|
|
1227
|
+
config.lanIp = null;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
if (!config.useHttps) {
|
|
1231
|
+
config.customCertPath = null;
|
|
1232
|
+
config.customKeyPath = null;
|
|
1233
|
+
}
|
|
1234
|
+
return config;
|
|
1235
|
+
}
|
|
1236
|
+
function readCurrentProxyConfig(dir) {
|
|
1237
|
+
const lanIp = readLanMarker(dir);
|
|
1238
|
+
const tld = readTldFromDir(dir);
|
|
1239
|
+
return {
|
|
1240
|
+
useHttps: readTlsMarker(dir),
|
|
1241
|
+
customCertPath: null,
|
|
1242
|
+
customKeyPath: null,
|
|
1243
|
+
lanMode: lanIp !== null || tld === "local",
|
|
1244
|
+
lanIp,
|
|
1245
|
+
lanIpExplicit: false,
|
|
1246
|
+
tld,
|
|
1247
|
+
useWildcard: false
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
function getProxyConfigMismatchMessages(desiredConfig, actualConfig, explicit) {
|
|
1251
|
+
const messages = [];
|
|
1252
|
+
if (explicit.lanMode && desiredConfig.lanMode !== actualConfig.lanMode) {
|
|
1253
|
+
messages.push(
|
|
1254
|
+
desiredConfig.lanMode ? "requested LAN mode, but the running proxy is not using LAN mode" : "requested non-LAN mode, but the running proxy is using LAN mode"
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
if (explicit.lanIp && desiredConfig.lanIp !== actualConfig.lanIp) {
|
|
1258
|
+
messages.push(
|
|
1259
|
+
`requested LAN IP ${desiredConfig.lanIp}, but the running proxy is using ${actualConfig.lanIp ?? "auto-detected LAN mode"}`
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
1262
|
+
if (explicit.useHttps && desiredConfig.useHttps !== actualConfig.useHttps) {
|
|
1263
|
+
messages.push(
|
|
1264
|
+
desiredConfig.useHttps ? "requested HTTPS, but the running proxy is using HTTP" : "requested HTTP, but the running proxy is using HTTPS"
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
if (explicit.tld && desiredConfig.tld !== actualConfig.tld) {
|
|
1268
|
+
messages.push(
|
|
1269
|
+
`requested .${desiredConfig.tld}, but the running proxy is using .${actualConfig.tld}`
|
|
1270
|
+
);
|
|
1271
|
+
}
|
|
1272
|
+
return messages;
|
|
1273
|
+
}
|
|
1274
|
+
function formatProxyStartCommand(proxyPort, config) {
|
|
1275
|
+
const needsSudo = !isWindows && proxyPort < PRIVILEGED_PORT_THRESHOLD;
|
|
1276
|
+
const { args } = buildProxyStartConfig({
|
|
1277
|
+
useHttps: config.useHttps,
|
|
1278
|
+
customCertPath: config.customCertPath,
|
|
1279
|
+
customKeyPath: config.customKeyPath,
|
|
1280
|
+
lanMode: config.lanMode,
|
|
1281
|
+
lanIp: config.lanIpExplicit ? config.lanIp : null,
|
|
1282
|
+
lanIpExplicit: config.lanIpExplicit,
|
|
1283
|
+
tld: config.tld,
|
|
1284
|
+
useWildcard: config.useWildcard,
|
|
1285
|
+
includePort: proxyPort !== getDefaultPort(config.useHttps),
|
|
1286
|
+
proxyPort
|
|
1287
|
+
});
|
|
1288
|
+
return `${needsSudo ? "sudo " : ""}portless proxy start${args.length > 0 ? ` ${args.join(" ")}` : ""}`;
|
|
1289
|
+
}
|
|
1290
|
+
function printProxyConfigMismatch(proxyPort, desiredConfig, messages) {
|
|
1291
|
+
const needsSudo = !isWindows && proxyPort < PRIVILEGED_PORT_THRESHOLD;
|
|
1292
|
+
const portFlag = proxyPort !== getDefaultPort(desiredConfig.useHttps) ? ` -p ${proxyPort}` : "";
|
|
1293
|
+
console.error(
|
|
1294
|
+
chalk.yellow(`Proxy is already running on port ${proxyPort} with a different config.`)
|
|
1295
|
+
);
|
|
1296
|
+
for (const message of messages) {
|
|
1297
|
+
console.error(chalk.yellow(`- ${message}`));
|
|
1298
|
+
}
|
|
1299
|
+
console.error(chalk.blue("Stop it first, then restart with the desired settings:"));
|
|
1300
|
+
console.error(chalk.cyan(` ${needsSudo ? "sudo " : ""}portless proxy stop${portFlag}`));
|
|
1301
|
+
console.error(chalk.cyan(` ${formatProxyStartCommand(proxyPort, desiredConfig)}`));
|
|
1302
|
+
process.exit(1);
|
|
1303
|
+
}
|
|
775
1304
|
function getEntryScript() {
|
|
776
1305
|
const script = process.argv[1];
|
|
777
1306
|
if (!script) {
|
|
@@ -782,10 +1311,10 @@ function getEntryScript() {
|
|
|
782
1311
|
function isLocallyInstalled() {
|
|
783
1312
|
let dir = process.cwd();
|
|
784
1313
|
for (; ; ) {
|
|
785
|
-
if (
|
|
1314
|
+
if (fs4.existsSync(path4.join(dir, "node_modules", "portless", "package.json"))) {
|
|
786
1315
|
return true;
|
|
787
1316
|
}
|
|
788
|
-
const parent =
|
|
1317
|
+
const parent = path4.dirname(dir);
|
|
789
1318
|
if (parent === dir) break;
|
|
790
1319
|
dir = parent;
|
|
791
1320
|
}
|
|
@@ -803,21 +1332,29 @@ function collectPortlessEnvArgs() {
|
|
|
803
1332
|
function sudoStop(port) {
|
|
804
1333
|
const stopArgs = [process.execPath, getEntryScript(), "proxy", "stop", "-p", String(port)];
|
|
805
1334
|
console.log(colors_default.yellow("Proxy is running as root. Elevating with sudo to stop it..."));
|
|
806
|
-
const result =
|
|
1335
|
+
const result = spawnSync2("sudo", ["env", ...collectPortlessEnvArgs(), ...stopArgs], {
|
|
807
1336
|
stdio: "inherit",
|
|
808
1337
|
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
809
1338
|
});
|
|
810
1339
|
return result.status === 0;
|
|
811
1340
|
}
|
|
812
|
-
function startProxyServer(store, proxyPort, tld, tlsOptions, strict) {
|
|
1341
|
+
function startProxyServer(store, proxyPort, tld, tlsOptions, lanIp, strict) {
|
|
813
1342
|
store.ensureDir();
|
|
814
1343
|
const isTls = !!tlsOptions;
|
|
1344
|
+
const mdnsSupport = isMdnsSupported();
|
|
1345
|
+
let activeLanIp = lanIp && mdnsSupport.supported ? lanIp : null;
|
|
1346
|
+
const lanIpPinned = !!process.env.PORTLESS_LAN_IP;
|
|
1347
|
+
let lanMonitor = null;
|
|
1348
|
+
if (lanIp && !mdnsSupport.supported) {
|
|
1349
|
+
const reason = mdnsSupport.reason ?? "mDNS publishing is not supported on this platform.";
|
|
1350
|
+
console.warn(chalk.yellow(`LAN mode disabled: ${reason}`));
|
|
1351
|
+
}
|
|
815
1352
|
const routesPath = store.getRoutesPath();
|
|
816
|
-
if (!
|
|
817
|
-
|
|
1353
|
+
if (!fs4.existsSync(routesPath)) {
|
|
1354
|
+
fs4.writeFileSync(routesPath, "[]", { mode: FILE_MODE });
|
|
818
1355
|
}
|
|
819
1356
|
try {
|
|
820
|
-
|
|
1357
|
+
fs4.chmodSync(routesPath, FILE_MODE);
|
|
821
1358
|
} catch {
|
|
822
1359
|
}
|
|
823
1360
|
fixOwnership(routesPath);
|
|
@@ -825,19 +1362,59 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, strict) {
|
|
|
825
1362
|
let debounceTimer = null;
|
|
826
1363
|
let watcher = null;
|
|
827
1364
|
let pollingInterval = null;
|
|
828
|
-
const
|
|
829
|
-
const
|
|
1365
|
+
const autoSyncHosts = shouldAutoSyncHosts(process.env.PORTLESS_SYNC_HOSTS);
|
|
1366
|
+
const onMdnsError = (msg) => console.warn(chalk.yellow(msg));
|
|
1367
|
+
const publishCachedRoutes = () => {
|
|
1368
|
+
if (!activeLanIp) return;
|
|
1369
|
+
for (const route of cachedRoutes) {
|
|
1370
|
+
publish(route.hostname, proxyPort, activeLanIp, onMdnsError);
|
|
1371
|
+
}
|
|
1372
|
+
};
|
|
1373
|
+
const updateLanIp = (nextIp, previousIp = activeLanIp) => {
|
|
1374
|
+
if (nextIp === activeLanIp) return;
|
|
1375
|
+
if (activeLanIp) {
|
|
1376
|
+
cleanupAll();
|
|
1377
|
+
}
|
|
1378
|
+
activeLanIp = nextIp;
|
|
1379
|
+
writeLanMarker(store.dir, activeLanIp);
|
|
1380
|
+
if (previousIp && nextIp) {
|
|
1381
|
+
console.log(chalk.green(`LAN IP changed: ${previousIp} -> ${nextIp}`));
|
|
1382
|
+
} else if (previousIp && !nextIp) {
|
|
1383
|
+
console.warn(chalk.yellow("LAN mode temporarily unavailable: no active LAN IP"));
|
|
1384
|
+
} else if (!previousIp && nextIp) {
|
|
1385
|
+
console.log(chalk.green(`LAN mode restored: ${nextIp}`));
|
|
1386
|
+
}
|
|
1387
|
+
publishCachedRoutes();
|
|
1388
|
+
};
|
|
830
1389
|
const reloadRoutes = () => {
|
|
831
1390
|
try {
|
|
1391
|
+
const previousRoutes = new Map(cachedRoutes.map((r) => [r.hostname, r.port]));
|
|
832
1392
|
cachedRoutes = store.loadRoutes();
|
|
833
1393
|
if (autoSyncHosts) {
|
|
834
1394
|
syncHostsFile(cachedRoutes.map((r) => r.hostname));
|
|
835
1395
|
}
|
|
1396
|
+
if (activeLanIp) {
|
|
1397
|
+
const currentRoutes = new Map(cachedRoutes.map((r) => [r.hostname, r.port]));
|
|
1398
|
+
for (const route of cachedRoutes) {
|
|
1399
|
+
const previousPort = previousRoutes.get(route.hostname);
|
|
1400
|
+
if (previousPort === void 0) {
|
|
1401
|
+
publish(route.hostname, proxyPort, activeLanIp, onMdnsError);
|
|
1402
|
+
} else if (previousPort !== route.port) {
|
|
1403
|
+
unpublish(route.hostname);
|
|
1404
|
+
publish(route.hostname, proxyPort, activeLanIp, onMdnsError);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
for (const hostname of previousRoutes.keys()) {
|
|
1408
|
+
if (!currentRoutes.has(hostname)) {
|
|
1409
|
+
unpublish(hostname);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
836
1413
|
} catch {
|
|
837
1414
|
}
|
|
838
1415
|
};
|
|
839
1416
|
try {
|
|
840
|
-
watcher =
|
|
1417
|
+
watcher = fs4.watch(routesPath, () => {
|
|
841
1418
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
842
1419
|
debounceTimer = setTimeout(reloadRoutes, DEBOUNCE_MS);
|
|
843
1420
|
});
|
|
@@ -848,6 +1425,7 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, strict) {
|
|
|
848
1425
|
if (autoSyncHosts) {
|
|
849
1426
|
syncHostsFile(cachedRoutes.map((r) => r.hostname));
|
|
850
1427
|
}
|
|
1428
|
+
publishCachedRoutes();
|
|
851
1429
|
const server = createProxyServer({
|
|
852
1430
|
getRoutes: () => cachedRoutes,
|
|
853
1431
|
proxyPort,
|
|
@@ -886,10 +1464,11 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, strict) {
|
|
|
886
1464
|
redirectServer.listen(80);
|
|
887
1465
|
}
|
|
888
1466
|
server.listen(proxyPort, () => {
|
|
889
|
-
|
|
890
|
-
|
|
1467
|
+
fs4.writeFileSync(store.pidPath, process.pid.toString(), { mode: FILE_MODE });
|
|
1468
|
+
fs4.writeFileSync(store.portFilePath, proxyPort.toString(), { mode: FILE_MODE });
|
|
891
1469
|
writeTlsMarker(store.dir, isTls);
|
|
892
1470
|
writeTldFile(store.dir, tld);
|
|
1471
|
+
writeLanMarker(store.dir, activeLanIp);
|
|
893
1472
|
fixOwnership(store.dir, store.pidPath, store.portFilePath);
|
|
894
1473
|
const proto = isTls ? "HTTPS/2" : "HTTP";
|
|
895
1474
|
const tldLabel = tld !== DEFAULT_TLD ? ` (TLD: .${tld})` : "";
|
|
@@ -897,6 +1476,24 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, strict) {
|
|
|
897
1476
|
console.log(
|
|
898
1477
|
colors_default.green(`${proto} proxy listening on port ${proxyPort}${tldLabel}${modeLabel}`)
|
|
899
1478
|
);
|
|
1479
|
+
if (activeLanIp) {
|
|
1480
|
+
console.log(chalk.green(`LAN mode: ${activeLanIp}`));
|
|
1481
|
+
console.log(chalk.gray("Services are discoverable as <name>.local on your network"));
|
|
1482
|
+
if (isTls) {
|
|
1483
|
+
console.log(chalk.yellow("For HTTPS on devices, install the CA certificate:"));
|
|
1484
|
+
console.log(chalk.gray(` ${path4.join(store.dir, "ca.pem")}`));
|
|
1485
|
+
}
|
|
1486
|
+
if (!lanIpPinned) {
|
|
1487
|
+
lanMonitor = startLanIpMonitor({
|
|
1488
|
+
initialIp: activeLanIp,
|
|
1489
|
+
onChange: (nextIp, previousIp) => updateLanIp(nextIp, previousIp),
|
|
1490
|
+
onError: (error) => {
|
|
1491
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1492
|
+
console.warn(chalk.yellow(`Failed to refresh LAN IP: ${message}`));
|
|
1493
|
+
}
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
900
1497
|
if (redirectServer) {
|
|
901
1498
|
console.log(colors_default.green("HTTP-to-HTTPS redirect listening on port 80"));
|
|
902
1499
|
}
|
|
@@ -907,18 +1504,20 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, strict) {
|
|
|
907
1504
|
exiting = true;
|
|
908
1505
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
909
1506
|
if (pollingInterval) clearInterval(pollingInterval);
|
|
1507
|
+
if (lanMonitor) lanMonitor.stop();
|
|
910
1508
|
if (watcher) {
|
|
911
1509
|
watcher.close();
|
|
912
1510
|
}
|
|
1511
|
+
if (activeLanIp) cleanupAll();
|
|
913
1512
|
if (redirectServer) {
|
|
914
1513
|
redirectServer.close();
|
|
915
1514
|
}
|
|
916
1515
|
try {
|
|
917
|
-
|
|
1516
|
+
fs4.unlinkSync(store.pidPath);
|
|
918
1517
|
} catch {
|
|
919
1518
|
}
|
|
920
1519
|
try {
|
|
921
|
-
|
|
1520
|
+
fs4.unlinkSync(store.portFilePath);
|
|
922
1521
|
} catch {
|
|
923
1522
|
}
|
|
924
1523
|
writeTlsMarker(store.dir, false);
|
|
@@ -947,7 +1546,7 @@ function sudoStopOrHint(port) {
|
|
|
947
1546
|
}
|
|
948
1547
|
async function stopProxy(store, proxyPort, _tls) {
|
|
949
1548
|
const pidPath = store.pidPath;
|
|
950
|
-
if (!
|
|
1549
|
+
if (!fs4.existsSync(pidPath)) {
|
|
951
1550
|
if (await isProxyRunning(proxyPort)) {
|
|
952
1551
|
console.log(colors_default.yellow(`PID file is missing but port ${proxyPort} is still in use.`));
|
|
953
1552
|
const pid = findPidOnPort(proxyPort);
|
|
@@ -955,7 +1554,7 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
955
1554
|
try {
|
|
956
1555
|
process.kill(pid, "SIGTERM");
|
|
957
1556
|
try {
|
|
958
|
-
|
|
1557
|
+
fs4.unlinkSync(store.portFilePath);
|
|
959
1558
|
} catch {
|
|
960
1559
|
}
|
|
961
1560
|
console.log(colors_default.green(`Killed process ${pid}. Proxy stopped.`));
|
|
@@ -990,10 +1589,10 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
990
1589
|
return;
|
|
991
1590
|
}
|
|
992
1591
|
try {
|
|
993
|
-
const pid = parseInt(
|
|
1592
|
+
const pid = parseInt(fs4.readFileSync(pidPath, "utf-8"), 10);
|
|
994
1593
|
if (isNaN(pid)) {
|
|
995
1594
|
console.error(colors_default.red("Corrupted PID file. Removing it."));
|
|
996
|
-
|
|
1595
|
+
fs4.unlinkSync(pidPath);
|
|
997
1596
|
return;
|
|
998
1597
|
}
|
|
999
1598
|
try {
|
|
@@ -1004,9 +1603,9 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
1004
1603
|
return;
|
|
1005
1604
|
}
|
|
1006
1605
|
console.log(colors_default.yellow("Proxy process is no longer running. Cleaning up stale files."));
|
|
1007
|
-
|
|
1606
|
+
fs4.unlinkSync(pidPath);
|
|
1008
1607
|
try {
|
|
1009
|
-
|
|
1608
|
+
fs4.unlinkSync(store.portFilePath);
|
|
1010
1609
|
} catch {
|
|
1011
1610
|
}
|
|
1012
1611
|
return;
|
|
@@ -1018,13 +1617,13 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
1018
1617
|
)
|
|
1019
1618
|
);
|
|
1020
1619
|
console.log(colors_default.yellow("Removing stale PID file."));
|
|
1021
|
-
|
|
1620
|
+
fs4.unlinkSync(pidPath);
|
|
1022
1621
|
return;
|
|
1023
1622
|
}
|
|
1024
1623
|
process.kill(pid, "SIGTERM");
|
|
1025
|
-
|
|
1624
|
+
fs4.unlinkSync(pidPath);
|
|
1026
1625
|
try {
|
|
1027
|
-
|
|
1626
|
+
fs4.unlinkSync(store.portFilePath);
|
|
1028
1627
|
} catch {
|
|
1029
1628
|
}
|
|
1030
1629
|
console.log(colors_default.green("Proxy stopped."));
|
|
@@ -1060,8 +1659,11 @@ function listRoutes(store, proxyPort, tls2) {
|
|
|
1060
1659
|
}
|
|
1061
1660
|
console.log();
|
|
1062
1661
|
}
|
|
1063
|
-
async function runApp(initialStore, proxyPort, stateDir, name, commandArgs, tls2, tld, force, autoInfo, desiredPort) {
|
|
1662
|
+
async function runApp(initialStore, proxyPort, stateDir, name, commandArgs, tls2, tld, force, autoInfo, desiredPort, lanMode = false, lanIp) {
|
|
1064
1663
|
let store = initialStore;
|
|
1664
|
+
console.log(chalk.blue.bold(`
|
|
1665
|
+
portless
|
|
1666
|
+
`));
|
|
1065
1667
|
let envTld;
|
|
1066
1668
|
try {
|
|
1067
1669
|
envTld = getDefaultTld();
|
|
@@ -1069,38 +1671,44 @@ async function runApp(initialStore, proxyPort, stateDir, name, commandArgs, tls2
|
|
|
1069
1671
|
console.error(colors_default.red(`Error: ${err.message}`));
|
|
1070
1672
|
process.exit(1);
|
|
1071
1673
|
}
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1674
|
+
const explicit = {
|
|
1675
|
+
useHttps: process.env.PORTLESS_HTTPS !== void 0,
|
|
1676
|
+
customCert: false,
|
|
1677
|
+
lanMode: process.env.PORTLESS_LAN !== void 0,
|
|
1678
|
+
lanIp: process.env.PORTLESS_LAN_IP !== void 0,
|
|
1679
|
+
tld: process.env.PORTLESS_TLD !== void 0,
|
|
1680
|
+
useWildcard: process.env.PORTLESS_WILDCARD !== void 0
|
|
1681
|
+
};
|
|
1682
|
+
const desiredConfig = resolveProxyConfig({
|
|
1683
|
+
persistedLanMode: lanMode,
|
|
1684
|
+
explicit,
|
|
1685
|
+
defaultTld: envTld,
|
|
1686
|
+
useHttps: !isHttpsEnvDisabled(),
|
|
1687
|
+
customCertPath: null,
|
|
1688
|
+
customKeyPath: null,
|
|
1689
|
+
lanMode: isLanEnvEnabled(),
|
|
1690
|
+
lanIp: process.env.PORTLESS_LAN_IP || null,
|
|
1691
|
+
tld: envTld,
|
|
1692
|
+
useWildcard: isWildcardEnvEnabled()
|
|
1693
|
+
});
|
|
1694
|
+
parseHostname(name, tld);
|
|
1695
|
+
const proxyResponsive = await isProxyRunning(proxyPort, tls2);
|
|
1696
|
+
const proxyListeningFromStateDir = !!process.env.PORTLESS_STATE_DIR && await isPortListening(proxyPort);
|
|
1697
|
+
if (!proxyResponsive && !proxyListeningFromStateDir) {
|
|
1698
|
+
const defaultPort = getDefaultPort(desiredConfig.useHttps);
|
|
1093
1699
|
const needsSudo = !isWindows && defaultPort < PRIVILEGED_PORT_THRESHOLD;
|
|
1700
|
+
const manualStartCommand = formatProxyStartCommand(defaultPort, desiredConfig);
|
|
1701
|
+
const fallbackStartCommand = formatProxyStartCommand(FALLBACK_PROXY_PORT, desiredConfig);
|
|
1094
1702
|
if (needsSudo && !process.stdin.isTTY) {
|
|
1095
1703
|
console.error(colors_default.red("Proxy is not running and no TTY is available for sudo."));
|
|
1096
1704
|
console.error(colors_default.blue("Option 1: start the proxy in a terminal (will prompt for sudo):"));
|
|
1097
|
-
console.error(colors_default.cyan(
|
|
1705
|
+
console.error(colors_default.cyan(` ${manualStartCommand}`));
|
|
1098
1706
|
console.error(
|
|
1099
1707
|
colors_default.blue(
|
|
1100
1708
|
`Option 2: use an unprivileged port (no sudo needed, URLs will include :${FALLBACK_PROXY_PORT}):`
|
|
1101
1709
|
)
|
|
1102
1710
|
);
|
|
1103
|
-
console.error(colors_default.cyan(`
|
|
1711
|
+
console.error(colors_default.cyan(` ${fallbackStartCommand}`));
|
|
1104
1712
|
process.exit(1);
|
|
1105
1713
|
}
|
|
1106
1714
|
if (needsSudo && process.stdin.isTTY) {
|
|
@@ -1116,15 +1724,23 @@ portless
|
|
|
1116
1724
|
}
|
|
1117
1725
|
}
|
|
1118
1726
|
console.log(colors_default.yellow("Starting proxy..."));
|
|
1119
|
-
const
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1727
|
+
const proxyStartConfig = buildProxyStartConfig({
|
|
1728
|
+
useHttps: desiredConfig.useHttps,
|
|
1729
|
+
customCertPath: desiredConfig.customCertPath,
|
|
1730
|
+
customKeyPath: desiredConfig.customKeyPath,
|
|
1731
|
+
lanMode: desiredConfig.lanMode,
|
|
1732
|
+
lanIp: desiredConfig.lanIpExplicit ? desiredConfig.lanIp : null,
|
|
1733
|
+
lanIpExplicit: desiredConfig.lanIpExplicit,
|
|
1734
|
+
tld: desiredConfig.tld,
|
|
1735
|
+
useWildcard: desiredConfig.useWildcard
|
|
1736
|
+
});
|
|
1737
|
+
const startArgs = [getEntryScript(), "proxy", "start", ...proxyStartConfig.args];
|
|
1738
|
+
const result = spawnSync2(process.execPath, startArgs, {
|
|
1123
1739
|
stdio: "inherit",
|
|
1124
1740
|
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
1125
1741
|
});
|
|
1126
1742
|
let discovered = null;
|
|
1127
|
-
if (result.
|
|
1743
|
+
if (!result.signal) {
|
|
1128
1744
|
for (let i = 0; i < WAIT_FOR_PROXY_MAX_ATTEMPTS; i++) {
|
|
1129
1745
|
await new Promise((r) => setTimeout(r, WAIT_FOR_PROXY_INTERVAL_MS));
|
|
1130
1746
|
const state = await discoverState();
|
|
@@ -1136,11 +1752,11 @@ portless
|
|
|
1136
1752
|
}
|
|
1137
1753
|
if (!discovered) {
|
|
1138
1754
|
console.error(colors_default.red("Failed to start proxy."));
|
|
1139
|
-
const fallbackDir = resolveStateDir(getDefaultPort(
|
|
1140
|
-
const logPath =
|
|
1755
|
+
const fallbackDir = resolveStateDir(getDefaultPort(desiredConfig.useHttps));
|
|
1756
|
+
const logPath = path4.join(fallbackDir, "proxy.log");
|
|
1141
1757
|
console.error(colors_default.blue("Try starting it manually:"));
|
|
1142
|
-
console.error(colors_default.cyan(
|
|
1143
|
-
if (
|
|
1758
|
+
console.error(colors_default.cyan(` ${manualStartCommand}`));
|
|
1759
|
+
if (fs4.existsSync(logPath)) {
|
|
1144
1760
|
console.error(colors_default.gray(`Logs: ${logPath}`));
|
|
1145
1761
|
}
|
|
1146
1762
|
process.exit(1);
|
|
@@ -1150,14 +1766,42 @@ portless
|
|
|
1150
1766
|
stateDir = discovered.dir;
|
|
1151
1767
|
tld = discovered.tld;
|
|
1152
1768
|
tls2 = discovered.tls;
|
|
1769
|
+
lanMode = discovered.lanMode;
|
|
1770
|
+
lanIp = discovered.lanIp;
|
|
1153
1771
|
store = new RouteStore(stateDir, {
|
|
1154
1772
|
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
1155
1773
|
});
|
|
1156
1774
|
console.log(colors_default.green("Proxy started in background"));
|
|
1157
1775
|
} else {
|
|
1158
|
-
|
|
1776
|
+
const runningConfig = readCurrentProxyConfig(stateDir);
|
|
1777
|
+
const mismatchMessages = getProxyConfigMismatchMessages(desiredConfig, runningConfig, explicit);
|
|
1778
|
+
if (mismatchMessages.length > 0) {
|
|
1779
|
+
printProxyConfigMismatch(proxyPort, desiredConfig, mismatchMessages);
|
|
1780
|
+
}
|
|
1781
|
+
lanMode = runningConfig.lanMode;
|
|
1782
|
+
lanIp = runningConfig.lanIp;
|
|
1783
|
+
console.log(chalk.gray("-- Proxy is running"));
|
|
1159
1784
|
}
|
|
1160
1785
|
const hostname = parseHostname(name, tld);
|
|
1786
|
+
if (envTld !== DEFAULT_TLD && envTld !== tld) {
|
|
1787
|
+
console.warn(
|
|
1788
|
+
chalk.yellow(
|
|
1789
|
+
`Warning: PORTLESS_TLD=${envTld} but the running proxy uses .${tld}. Using .${tld}.`
|
|
1790
|
+
)
|
|
1791
|
+
);
|
|
1792
|
+
}
|
|
1793
|
+
if (lanIp) {
|
|
1794
|
+
console.log(chalk.gray(`-- ${hostname} (LAN: ${lanIp})`));
|
|
1795
|
+
} else {
|
|
1796
|
+
console.log(chalk.gray(`-- ${hostname} (auto-resolves to 127.0.0.1)`));
|
|
1797
|
+
}
|
|
1798
|
+
if (autoInfo) {
|
|
1799
|
+
const baseName = autoInfo.prefix ? name.slice(autoInfo.prefix.length + 1) : name;
|
|
1800
|
+
console.log(chalk.gray(`-- Name "${baseName}" (from ${autoInfo.nameSource})`));
|
|
1801
|
+
if (autoInfo.prefix) {
|
|
1802
|
+
console.log(chalk.gray(`-- Prefix "${autoInfo.prefix}" (from ${autoInfo.prefixSource})`));
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1161
1805
|
const port = desiredPort ?? await findFreePort();
|
|
1162
1806
|
if (desiredPort) {
|
|
1163
1807
|
console.log(colors_default.green(`-- Using port ${port} (fixed)`));
|
|
@@ -1178,13 +1822,24 @@ portless
|
|
|
1178
1822
|
console.log(colors_default.yellow(`Killed existing process (PID ${killedPid})`));
|
|
1179
1823
|
}
|
|
1180
1824
|
const finalUrl = formatUrl(hostname, proxyPort, tls2);
|
|
1181
|
-
console.log(
|
|
1825
|
+
console.log(chalk.cyan.bold(`
|
|
1182
1826
|
-> ${finalUrl}
|
|
1183
1827
|
`));
|
|
1828
|
+
if (lanIp) {
|
|
1829
|
+
console.log(chalk.green(` LAN -> ${finalUrl}`));
|
|
1830
|
+
console.log(chalk.gray(" (accessible from other devices on the same WiFi network)\n"));
|
|
1831
|
+
}
|
|
1832
|
+
const basename3 = path4.basename(commandArgs[0]);
|
|
1833
|
+
const isExpo = basename3 === "expo";
|
|
1834
|
+
const isExpoLan = isExpo && (lanMode || isLanEnvEnabled());
|
|
1835
|
+
const hostBind = isExpoLan ? void 0 : "127.0.0.1";
|
|
1836
|
+
if (lanMode && !process.env.PORTLESS_LAN) {
|
|
1837
|
+
process.env.PORTLESS_LAN = "1";
|
|
1838
|
+
}
|
|
1184
1839
|
injectFrameworkFlags(commandArgs, port);
|
|
1185
1840
|
console.log(
|
|
1186
|
-
|
|
1187
|
-
`Running: PORT=${port} HOST
|
|
1841
|
+
chalk.gray(
|
|
1842
|
+
`Running: PORT=${port}${hostBind ? ` HOST=${hostBind}` : ""} PORTLESS_URL=${finalUrl} ${commandArgs.join(" ")}
|
|
1188
1843
|
`
|
|
1189
1844
|
)
|
|
1190
1845
|
);
|
|
@@ -1192,9 +1847,13 @@ portless
|
|
|
1192
1847
|
env: {
|
|
1193
1848
|
...process.env,
|
|
1194
1849
|
PORT: port.toString(),
|
|
1195
|
-
HOST:
|
|
1850
|
+
...hostBind ? { HOST: hostBind } : {},
|
|
1196
1851
|
PORTLESS_URL: finalUrl,
|
|
1197
|
-
__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS: `.${tld}
|
|
1852
|
+
__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS: `.${tld}`,
|
|
1853
|
+
// Note: EXPO_PACKAGER_PROXY_URL is not used — expo-dev-client removed
|
|
1854
|
+
// baked-in pinging, making this env var ineffective. Expo handles its
|
|
1855
|
+
// own LAN discovery natively.
|
|
1856
|
+
...lanMode ? { PORTLESS_LAN: "1" } : {}
|
|
1198
1857
|
},
|
|
1199
1858
|
onCleanup: () => {
|
|
1200
1859
|
try {
|
|
@@ -1342,6 +2001,7 @@ ${colors_default.bold("Install:")}
|
|
|
1342
2001
|
${colors_default.bold("Usage:")}
|
|
1343
2002
|
${colors_default.cyan("portless proxy start")} Start the proxy (HTTPS on port 443, daemon)
|
|
1344
2003
|
${colors_default.cyan("portless proxy start --no-tls")} Start without HTTPS (port 80)
|
|
2004
|
+
${colors_default.cyan("portless proxy start --lan")} Start in LAN mode (mDNS for real device testing)
|
|
1345
2005
|
${colors_default.cyan("portless proxy start -p 1355")} Start on a custom port (no sudo)
|
|
1346
2006
|
${colors_default.cyan("portless proxy stop")} Stop the proxy
|
|
1347
2007
|
${colors_default.cyan("portless <name> <cmd>")} Run your app through the proxy
|
|
@@ -1351,6 +2011,7 @@ ${colors_default.bold("Usage:")}
|
|
|
1351
2011
|
${colors_default.cyan("portless alias --remove <name>")} Remove a static route
|
|
1352
2012
|
${colors_default.cyan("portless list")} Show active routes
|
|
1353
2013
|
${colors_default.cyan("portless trust")} Add local CA to system trust store
|
|
2014
|
+
${colors_default.cyan("portless clean")} Remove portless state, trust entry, and hosts block
|
|
1354
2015
|
${colors_default.cyan("portless hosts sync")} Add routes to ${HOSTS_DISPLAY} (fixes Safari)
|
|
1355
2016
|
${colors_default.cyan("portless hosts clean")} Remove portless entries from ${HOSTS_DISPLAY}
|
|
1356
2017
|
|
|
@@ -1378,14 +2039,30 @@ ${colors_default.bold("How it works:")}
|
|
|
1378
2039
|
(apps get a random port in the 4000-4999 range via PORT)
|
|
1379
2040
|
3. Access via https://<name>.localhost
|
|
1380
2041
|
4. .localhost domains auto-resolve to 127.0.0.1
|
|
1381
|
-
5. Frameworks that ignore PORT (Vite, Astro, React Router, Angular,
|
|
1382
|
-
Expo, React Native) get --port and --host flags
|
|
2042
|
+
5. Frameworks that ignore PORT (Vite, VitePlus, Astro, React Router, Angular,
|
|
2043
|
+
Expo, React Native) get --port and, when needed, --host flags
|
|
2044
|
+
injected automatically
|
|
1383
2045
|
|
|
1384
2046
|
${colors_default.bold("HTTP/2 + HTTPS (default):")}
|
|
1385
2047
|
HTTPS with HTTP/2 multiplexing is enabled by default (faster page loads).
|
|
1386
2048
|
On first use, portless generates a local CA and adds it to your
|
|
1387
2049
|
system trust store. No browser warnings. Disable with --no-tls.
|
|
1388
2050
|
|
|
2051
|
+
${colors_default.bold("LAN mode:")}
|
|
2052
|
+
Use --lan to make services accessible from other devices (phones,
|
|
2053
|
+
tablets) on the same WiFi network via mDNS (.local domains).
|
|
2054
|
+
Useful for testing React Native / Expo apps on real devices.
|
|
2055
|
+
Expo keeps Metro's default LAN host behavior in this mode.
|
|
2056
|
+
Auto-detected LAN IPs follow network changes automatically.
|
|
2057
|
+
Stopped LAN proxies keep LAN mode for the next start via proxy.lan.
|
|
2058
|
+
Other proxy settings still follow the current flags and env vars.
|
|
2059
|
+
Use PORTLESS_LAN=0 for one start to switch back to .localhost mode.
|
|
2060
|
+
If a proxy is already running with different explicit LAN/TLS/TLD settings,
|
|
2061
|
+
stop it first.
|
|
2062
|
+
${colors_default.cyan("portless proxy start --lan")}
|
|
2063
|
+
${colors_default.cyan("portless proxy start --lan --https")}
|
|
2064
|
+
${colors_default.cyan("portless proxy start --lan --ip 192.168.1.42")}
|
|
2065
|
+
|
|
1389
2066
|
${colors_default.bold("Options:")}
|
|
1390
2067
|
run [--name <name>] <cmd> Infer project name (or override with --name)
|
|
1391
2068
|
Adds worktree prefix in git worktrees
|
|
@@ -1393,6 +2070,8 @@ ${colors_default.bold("Options:")}
|
|
|
1393
2070
|
Standard ports auto-elevate with sudo on macOS/Linux
|
|
1394
2071
|
--no-tls Disable HTTPS (use plain HTTP on port 80)
|
|
1395
2072
|
--https Enable HTTPS (default, accepted for compatibility)
|
|
2073
|
+
--lan Enable LAN mode (mDNS .local, for real device testing)
|
|
2074
|
+
--ip <address> Pin a specific LAN IP (disables auto-follow; use with --lan)
|
|
1396
2075
|
--cert <path> Use a custom TLS certificate
|
|
1397
2076
|
--key <path> Use a custom TLS private key
|
|
1398
2077
|
--foreground Run proxy in foreground (for debugging)
|
|
@@ -1406,23 +2085,25 @@ ${colors_default.bold("Options:")}
|
|
|
1406
2085
|
${colors_default.bold("Environment variables:")}
|
|
1407
2086
|
PORTLESS_PORT=<number> Override the default proxy port (e.g. in .bashrc)
|
|
1408
2087
|
PORTLESS_APP_PORT=<number> Use a fixed port for the app (same as --app-port)
|
|
1409
|
-
PORTLESS_HTTPS
|
|
2088
|
+
PORTLESS_HTTPS=0 Disable HTTPS (same as --no-tls)
|
|
2089
|
+
PORTLESS_LAN=1 Enable LAN mode when set to 1 (set in .bashrc / .zshrc)
|
|
1410
2090
|
PORTLESS_TLD=<tld> Use a custom TLD (e.g. test, dev; default: localhost)
|
|
1411
2091
|
PORTLESS_WILDCARD=1 Allow unregistered subdomains to fall back to parent route
|
|
1412
|
-
PORTLESS_SYNC_HOSTS=
|
|
2092
|
+
PORTLESS_SYNC_HOSTS=0 Disable auto-sync of ${HOSTS_DISPLAY} (on by default)
|
|
1413
2093
|
PORTLESS_STATE_DIR=<path> Override the state directory
|
|
1414
2094
|
PORTLESS=0 Run command directly without proxy
|
|
1415
2095
|
|
|
1416
2096
|
${colors_default.bold("Child process environment:")}
|
|
1417
2097
|
PORT Ephemeral port the child should listen on
|
|
1418
|
-
HOST
|
|
2098
|
+
HOST Usually 127.0.0.1 (omitted for Expo in LAN mode)
|
|
1419
2099
|
PORTLESS_URL Public URL of the app (e.g. https://myapp.localhost)
|
|
2100
|
+
PORTLESS_LAN Set to 1 when proxy is in LAN mode
|
|
1420
2101
|
|
|
1421
2102
|
${colors_default.bold("Safari / DNS:")}
|
|
1422
2103
|
.localhost subdomains auto-resolve in Chrome, Firefox, and Edge.
|
|
1423
2104
|
Safari relies on the system DNS resolver, which may not handle them.
|
|
1424
|
-
Auto-syncs ${HOSTS_DISPLAY} for
|
|
1425
|
-
|
|
2105
|
+
Auto-syncs ${HOSTS_DISPLAY} for route hostnames by default (including .localhost,
|
|
2106
|
+
custom TLDs, and LAN .local). Set PORTLESS_SYNC_HOSTS=0 to disable. To manually sync:
|
|
1426
2107
|
${colors_default.cyan("portless hosts sync")}
|
|
1427
2108
|
Clean up later with:
|
|
1428
2109
|
${colors_default.cyan("portless hosts clean")}
|
|
@@ -1431,20 +2112,20 @@ ${colors_default.bold("Skip portless:")}
|
|
|
1431
2112
|
PORTLESS=0 pnpm dev # Runs command directly without proxy
|
|
1432
2113
|
|
|
1433
2114
|
${colors_default.bold("Reserved names:")}
|
|
1434
|
-
run, get, alias, hosts, list, trust, proxy are subcommands and cannot
|
|
2115
|
+
run, get, alias, hosts, list, trust, clean, proxy are subcommands and cannot
|
|
1435
2116
|
be used as app names directly. Use "portless run" to infer the name,
|
|
1436
2117
|
or "portless --name <name>" to force any name including reserved ones.
|
|
1437
2118
|
`);
|
|
1438
2119
|
process.exit(0);
|
|
1439
2120
|
}
|
|
1440
2121
|
function printVersion() {
|
|
1441
|
-
console.log("0.
|
|
2122
|
+
console.log("0.10.1");
|
|
1442
2123
|
process.exit(0);
|
|
1443
2124
|
}
|
|
1444
2125
|
async function handleTrust() {
|
|
1445
2126
|
const { dir } = await discoverState();
|
|
1446
|
-
if (!
|
|
1447
|
-
|
|
2127
|
+
if (!fs4.existsSync(dir)) {
|
|
2128
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
1448
2129
|
}
|
|
1449
2130
|
const { caGenerated } = ensureCerts(dir);
|
|
1450
2131
|
if (caGenerated) {
|
|
@@ -1459,7 +2140,7 @@ async function handleTrust() {
|
|
|
1459
2140
|
const isPermissionError = result.error?.includes("Permission denied") || result.error?.includes("EACCES");
|
|
1460
2141
|
if (isPermissionError && !isWindows && process.getuid?.() !== 0) {
|
|
1461
2142
|
console.log(colors_default.yellow("Trusting the CA requires elevated privileges. Requesting sudo..."));
|
|
1462
|
-
const sudoResult =
|
|
2143
|
+
const sudoResult = spawnSync2(
|
|
1463
2144
|
"sudo",
|
|
1464
2145
|
[
|
|
1465
2146
|
"env",
|
|
@@ -1480,6 +2161,90 @@ async function handleTrust() {
|
|
|
1480
2161
|
console.error(colors_default.red(`Failed to trust CA: ${result.error}`));
|
|
1481
2162
|
process.exit(1);
|
|
1482
2163
|
}
|
|
2164
|
+
async function handleClean(args) {
|
|
2165
|
+
if (args[1] === "--help" || args[1] === "-h") {
|
|
2166
|
+
console.log(`
|
|
2167
|
+
${colors_default.bold("portless clean")} - Remove portless artifacts from this machine.
|
|
2168
|
+
|
|
2169
|
+
Stops the proxy if it is running, removes the local CA from the OS trust store
|
|
2170
|
+
when it was installed by portless, deletes known files under state directories
|
|
2171
|
+
(~/.portless, the system state directory, and PORTLESS_STATE_DIR when set),
|
|
2172
|
+
and removes the portless block from ${HOSTS_DISPLAY}.
|
|
2173
|
+
|
|
2174
|
+
Only allowlisted filenames under each state directory are deleted. Custom
|
|
2175
|
+
certificate paths from --cert and --key are never removed.
|
|
2176
|
+
|
|
2177
|
+
macOS/Linux may prompt for sudo when the proxy, trust store, or ${HOSTS_DISPLAY}
|
|
2178
|
+
require elevated privileges. On Windows, run as Administrator if needed.
|
|
2179
|
+
|
|
2180
|
+
${colors_default.bold("Usage:")}
|
|
2181
|
+
${colors_default.cyan("portless clean")}
|
|
2182
|
+
|
|
2183
|
+
${colors_default.bold("Options:")}
|
|
2184
|
+
--help, -h Show this help
|
|
2185
|
+
`);
|
|
2186
|
+
process.exit(0);
|
|
2187
|
+
}
|
|
2188
|
+
if (args.length > 1) {
|
|
2189
|
+
console.error(colors_default.red(`Error: Unknown argument "${args[1]}".`));
|
|
2190
|
+
console.error(colors_default.cyan(" portless clean --help"));
|
|
2191
|
+
process.exit(1);
|
|
2192
|
+
}
|
|
2193
|
+
console.log(colors_default.cyan("Stopping proxy if it is running..."));
|
|
2194
|
+
const { dir, port, tls: tls2 } = await discoverState();
|
|
2195
|
+
const store = new RouteStore(dir, {
|
|
2196
|
+
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
2197
|
+
});
|
|
2198
|
+
await stopProxy(store, port, tls2);
|
|
2199
|
+
const stateDirs = collectStateDirsForCleanup();
|
|
2200
|
+
for (const stateDir of stateDirs) {
|
|
2201
|
+
const caPath = path4.join(stateDir, "ca.pem");
|
|
2202
|
+
if (!fs4.existsSync(caPath)) continue;
|
|
2203
|
+
const wasTrusted = isCATrusted(stateDir);
|
|
2204
|
+
if (!wasTrusted) continue;
|
|
2205
|
+
const untrustResult = untrustCA(stateDir);
|
|
2206
|
+
if (untrustResult.removed) {
|
|
2207
|
+
console.log(colors_default.green("Removed local CA from the system trust store."));
|
|
2208
|
+
} else if (untrustResult.error) {
|
|
2209
|
+
console.warn(
|
|
2210
|
+
colors_default.yellow(
|
|
2211
|
+
`Could not remove CA from trust store: ${untrustResult.error}
|
|
2212
|
+
Try: sudo portless clean (Linux), or delete the certificate manually.`
|
|
2213
|
+
)
|
|
2214
|
+
);
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
for (const stateDir of stateDirs) {
|
|
2218
|
+
removePortlessStateFiles(stateDir);
|
|
2219
|
+
}
|
|
2220
|
+
console.log(colors_default.green("Removed portless state files from known state directories."));
|
|
2221
|
+
if (cleanHostsFile()) {
|
|
2222
|
+
console.log(colors_default.green(`Removed portless entries from ${HOSTS_DISPLAY}.`));
|
|
2223
|
+
} else if (!isWindows && process.getuid?.() !== 0) {
|
|
2224
|
+
console.log(
|
|
2225
|
+
colors_default.yellow(`Updating ${HOSTS_DISPLAY} requires elevated privileges. Requesting sudo...`)
|
|
2226
|
+
);
|
|
2227
|
+
const result = spawnSync2(
|
|
2228
|
+
"sudo",
|
|
2229
|
+
["env", ...collectPortlessEnvArgs(), process.execPath, getEntryScript(), "clean"],
|
|
2230
|
+
{
|
|
2231
|
+
stdio: "inherit",
|
|
2232
|
+
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
2233
|
+
}
|
|
2234
|
+
);
|
|
2235
|
+
if (result.status !== 0) {
|
|
2236
|
+
console.error(colors_default.red(`Failed to update ${HOSTS_DISPLAY}. Run: sudo portless clean`));
|
|
2237
|
+
process.exit(1);
|
|
2238
|
+
}
|
|
2239
|
+
} else {
|
|
2240
|
+
console.warn(
|
|
2241
|
+
colors_default.yellow(
|
|
2242
|
+
`Could not remove portless entries from ${HOSTS_DISPLAY}${isWindows ? " (run as Administrator)." : "."}`
|
|
2243
|
+
)
|
|
2244
|
+
);
|
|
2245
|
+
}
|
|
2246
|
+
console.log(colors_default.green("Clean finished."));
|
|
2247
|
+
}
|
|
1483
2248
|
async function handleList() {
|
|
1484
2249
|
const { dir, port, tls: tls2 } = await discoverState();
|
|
1485
2250
|
const store = new RouteStore(dir, {
|
|
@@ -1614,8 +2379,8 @@ ${colors_default.bold("Usage:")}
|
|
|
1614
2379
|
${colors_default.cyan("portless hosts clean")} Remove portless entries from ${HOSTS_DISPLAY}
|
|
1615
2380
|
|
|
1616
2381
|
${colors_default.bold("Auto-sync:")}
|
|
1617
|
-
|
|
1618
|
-
PORTLESS_SYNC_HOSTS=
|
|
2382
|
+
The proxy updates ${HOSTS_DISPLAY} for route hostnames by default. Disable with
|
|
2383
|
+
PORTLESS_SYNC_HOSTS=0.
|
|
1619
2384
|
`);
|
|
1620
2385
|
process.exit(0);
|
|
1621
2386
|
}
|
|
@@ -1630,7 +2395,7 @@ ${colors_default.bold("Auto-sync:")}
|
|
|
1630
2395
|
`Writing to ${HOSTS_DISPLAY} requires elevated privileges. Requesting sudo...`
|
|
1631
2396
|
)
|
|
1632
2397
|
);
|
|
1633
|
-
const result =
|
|
2398
|
+
const result = spawnSync2(
|
|
1634
2399
|
"sudo",
|
|
1635
2400
|
["env", ...collectPortlessEnvArgs(), process.execPath, getEntryScript(), "hosts", "clean"],
|
|
1636
2401
|
{
|
|
@@ -1683,7 +2448,7 @@ ${colors_default.bold("Usage: portless hosts <command>")}
|
|
|
1683
2448
|
console.log(
|
|
1684
2449
|
colors_default.yellow(`Writing to ${HOSTS_DISPLAY} requires elevated privileges. Requesting sudo...`)
|
|
1685
2450
|
);
|
|
1686
|
-
const result =
|
|
2451
|
+
const result = spawnSync2(
|
|
1687
2452
|
"sudo",
|
|
1688
2453
|
["env", ...collectPortlessEnvArgs(), process.execPath, getEntryScript(), "hosts", "sync"],
|
|
1689
2454
|
{
|
|
@@ -1734,15 +2499,25 @@ ${colors_default.bold("portless proxy")} - Manage the portless proxy server.
|
|
|
1734
2499
|
${colors_default.bold("Usage:")}
|
|
1735
2500
|
${colors_default.cyan("portless proxy start")} Start the HTTPS proxy on port 443 (daemon)
|
|
1736
2501
|
${colors_default.cyan("portless proxy start --no-tls")} Start without HTTPS (port 80)
|
|
2502
|
+
${colors_default.cyan("portless proxy start --lan")} Enable LAN mode (mDNS, .local TLD)
|
|
1737
2503
|
${colors_default.cyan("portless proxy start --foreground")} Start in foreground (for debugging)
|
|
1738
2504
|
${colors_default.cyan("portless proxy start -p 1355")} Start on a custom port (no sudo)
|
|
1739
2505
|
${colors_default.cyan("portless proxy start --tld test")} Use .test instead of .localhost
|
|
1740
2506
|
${colors_default.cyan("portless proxy start --wildcard")} Allow unregistered subdomains to fall back to parent
|
|
1741
2507
|
${colors_default.cyan("portless proxy stop")} Stop the proxy
|
|
2508
|
+
|
|
2509
|
+
${colors_default.bold("LAN mode (--lan):")}
|
|
2510
|
+
Makes services accessible from other devices on the same WiFi network
|
|
2511
|
+
via mDNS (.local domains). Useful for testing on real mobile devices.
|
|
2512
|
+
Auto-detects your LAN IP and follows changes automatically, or use
|
|
2513
|
+
--ip to pin one.
|
|
2514
|
+
Stopped LAN proxies keep LAN mode for the next start via proxy.lan.
|
|
2515
|
+
Use PORTLESS_LAN=0 for one start to switch back to .localhost mode.
|
|
1742
2516
|
`);
|
|
1743
2517
|
process.exit(isProxyHelp || !args[1] ? 0 : 1);
|
|
1744
2518
|
}
|
|
1745
2519
|
const isForeground = args.includes("--foreground");
|
|
2520
|
+
const hasHttpsFlag = args.includes("--https");
|
|
1746
2521
|
const hasNoTls = args.includes("--no-tls") || isHttpsEnvDisabled();
|
|
1747
2522
|
const wantHttps = !hasNoTls;
|
|
1748
2523
|
let customCertPath = null;
|
|
@@ -1767,7 +2542,7 @@ ${colors_default.bold("Usage:")}
|
|
|
1767
2542
|
console.error(colors_default.red("Error: --cert and --key must be used together."));
|
|
1768
2543
|
process.exit(1);
|
|
1769
2544
|
}
|
|
1770
|
-
|
|
2545
|
+
let useHttps = wantHttps || !!(customCertPath && customKeyPath);
|
|
1771
2546
|
let hasExplicitPort = false;
|
|
1772
2547
|
let proxyPort = getDefaultPort(useHttps);
|
|
1773
2548
|
let portFlagIndex = args.indexOf("--port");
|
|
@@ -1809,12 +2584,66 @@ ${colors_default.bold("Usage:")}
|
|
|
1809
2584
|
process.exit(1);
|
|
1810
2585
|
}
|
|
1811
2586
|
}
|
|
2587
|
+
const useWildcard = args.includes("--wildcard") || isWildcardEnvEnabled();
|
|
2588
|
+
const explicit = {
|
|
2589
|
+
useHttps: hasHttpsFlag || hasNoTls || customCertPath !== null || customKeyPath !== null || process.env.PORTLESS_HTTPS !== void 0,
|
|
2590
|
+
customCert: customCertPath !== null || customKeyPath !== null,
|
|
2591
|
+
lanMode: process.env.PORTLESS_LAN !== void 0,
|
|
2592
|
+
lanIp: process.env.PORTLESS_LAN_IP !== void 0,
|
|
2593
|
+
tld: tldIdx !== -1 || process.env.PORTLESS_TLD !== void 0,
|
|
2594
|
+
useWildcard: args.includes("--wildcard") || process.env.PORTLESS_WILDCARD !== void 0
|
|
2595
|
+
};
|
|
2596
|
+
let stateDir = resolveStateDir(proxyPort);
|
|
2597
|
+
let persistedLanMode = readLanMarker(stateDir) !== null;
|
|
2598
|
+
let runningPort = null;
|
|
2599
|
+
if (!hasExplicitPort) {
|
|
2600
|
+
const currentState = await discoverState();
|
|
2601
|
+
persistedLanMode = currentState.lanMode;
|
|
2602
|
+
if (await isProxyRunning(currentState.port) || !!process.env.PORTLESS_STATE_DIR && await isPortListening(currentState.port)) {
|
|
2603
|
+
runningPort = currentState.port;
|
|
2604
|
+
proxyPort = currentState.port;
|
|
2605
|
+
stateDir = currentState.dir;
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
const desiredConfig = resolveProxyConfig({
|
|
2609
|
+
persistedLanMode,
|
|
2610
|
+
explicit,
|
|
2611
|
+
defaultTld: getDefaultTld(),
|
|
2612
|
+
useHttps: wantHttps || !!(customCertPath && customKeyPath),
|
|
2613
|
+
customCertPath,
|
|
2614
|
+
customKeyPath,
|
|
2615
|
+
lanMode: isLanEnvEnabled(),
|
|
2616
|
+
lanIp: process.env.PORTLESS_LAN_IP || null,
|
|
2617
|
+
tld,
|
|
2618
|
+
useWildcard
|
|
2619
|
+
});
|
|
2620
|
+
const lanMode = desiredConfig.lanMode;
|
|
2621
|
+
useHttps = desiredConfig.useHttps;
|
|
2622
|
+
customCertPath = desiredConfig.customCertPath;
|
|
2623
|
+
customKeyPath = desiredConfig.customKeyPath;
|
|
2624
|
+
tld = desiredConfig.tld;
|
|
2625
|
+
const desiredWildcard = desiredConfig.useWildcard;
|
|
2626
|
+
let lanIp = desiredConfig.lanIpExplicit ? desiredConfig.lanIp : null;
|
|
2627
|
+
if (!hasExplicitPort && runningPort === null) {
|
|
2628
|
+
proxyPort = getDefaultPort(useHttps);
|
|
2629
|
+
stateDir = resolveStateDir(proxyPort);
|
|
2630
|
+
}
|
|
2631
|
+
if (lanMode && tldIdx !== -1) {
|
|
2632
|
+
const userTld = args[tldIdx + 1];
|
|
2633
|
+
if (userTld && userTld !== "local") {
|
|
2634
|
+
console.warn(
|
|
2635
|
+
chalk.yellow(
|
|
2636
|
+
`Warning: --lan forces .local TLD (mDNS requirement). Ignoring --tld ${userTld}.`
|
|
2637
|
+
)
|
|
2638
|
+
);
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
1812
2641
|
const riskyReason = RISKY_TLDS.get(tld);
|
|
1813
|
-
if (riskyReason) {
|
|
2642
|
+
if (riskyReason && !lanMode) {
|
|
1814
2643
|
console.warn(colors_default.yellow(`Warning: .${tld}: ${riskyReason}`));
|
|
1815
2644
|
}
|
|
1816
2645
|
const syncDisabled = process.env.PORTLESS_SYNC_HOSTS === "0" || process.env.PORTLESS_SYNC_HOSTS === "false";
|
|
1817
|
-
if (tld !== DEFAULT_TLD && syncDisabled) {
|
|
2646
|
+
if (tld !== DEFAULT_TLD && !lanMode && syncDisabled) {
|
|
1818
2647
|
console.warn(
|
|
1819
2648
|
colors_default.yellow(
|
|
1820
2649
|
`Warning: .${tld} domains require ${HOSTS_DISPLAY} entries to resolve to 127.0.0.1.`
|
|
@@ -1823,51 +2652,93 @@ ${colors_default.bold("Usage:")}
|
|
|
1823
2652
|
console.warn(colors_default.yellow("Hosts sync is disabled. To add entries manually, run:"));
|
|
1824
2653
|
console.warn(colors_default.cyan(" portless hosts sync"));
|
|
1825
2654
|
}
|
|
1826
|
-
const useWildcard = args.includes("--wildcard") || isWildcardEnvEnabled();
|
|
1827
|
-
let stateDir = resolveStateDir(proxyPort);
|
|
1828
2655
|
let store = new RouteStore(stateDir, {
|
|
1829
2656
|
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
1830
2657
|
});
|
|
1831
|
-
|
|
2658
|
+
const proxyRunning = runningPort !== null || await isProxyRunning(proxyPort);
|
|
2659
|
+
if (proxyRunning) {
|
|
2660
|
+
const runningConfig = readCurrentProxyConfig(stateDir);
|
|
2661
|
+
const mismatchMessages = getProxyConfigMismatchMessages(desiredConfig, runningConfig, explicit);
|
|
2662
|
+
if (mismatchMessages.length > 0) {
|
|
2663
|
+
printProxyConfigMismatch(proxyPort, desiredConfig, mismatchMessages);
|
|
2664
|
+
}
|
|
1832
2665
|
if (isForeground) {
|
|
1833
2666
|
return;
|
|
1834
2667
|
}
|
|
1835
|
-
const portFlag = proxyPort !==
|
|
2668
|
+
const portFlag = proxyPort !== getDefaultPort(useHttps) ? ` -p ${proxyPort}` : "";
|
|
1836
2669
|
console.log(colors_default.yellow(`Proxy is already running on port ${proxyPort}.`));
|
|
1837
2670
|
console.log(
|
|
1838
2671
|
colors_default.blue(`To restart: portless proxy stop${portFlag} && portless proxy start${portFlag}`)
|
|
1839
2672
|
);
|
|
1840
2673
|
return;
|
|
1841
2674
|
}
|
|
2675
|
+
if (lanMode) {
|
|
2676
|
+
const mdnsSupport = isMdnsSupported();
|
|
2677
|
+
if (!mdnsSupport.supported) {
|
|
2678
|
+
console.error(
|
|
2679
|
+
colors_default.red(
|
|
2680
|
+
"Error: LAN mode requires mDNS publishing, which is not supported on this platform."
|
|
2681
|
+
)
|
|
2682
|
+
);
|
|
2683
|
+
if (mdnsSupport.reason) {
|
|
2684
|
+
console.error(colors_default.gray(mdnsSupport.reason));
|
|
2685
|
+
}
|
|
2686
|
+
process.exit(1);
|
|
2687
|
+
}
|
|
2688
|
+
const inheritedLanIp = process.env[INTERNAL_LAN_IP_ENV] || null;
|
|
2689
|
+
delete process.env[INTERNAL_LAN_IP_ENV];
|
|
2690
|
+
if (!lanIp) {
|
|
2691
|
+
lanIp = inheritedLanIp || await getLocalNetworkIp();
|
|
2692
|
+
}
|
|
2693
|
+
if (!lanIp) {
|
|
2694
|
+
console.error(colors_default.red("Error: Could not detect LAN IP. Are you connected to a network?"));
|
|
2695
|
+
console.error(colors_default.blue("Specify manually:"));
|
|
2696
|
+
console.error(colors_default.cyan(" portless proxy start --lan --ip 192.168.1.42"));
|
|
2697
|
+
process.exit(1);
|
|
2698
|
+
}
|
|
2699
|
+
} else {
|
|
2700
|
+
delete process.env[INTERNAL_LAN_IP_ENV];
|
|
2701
|
+
}
|
|
2702
|
+
const resolvedConfig = {
|
|
2703
|
+
...desiredConfig,
|
|
2704
|
+
useHttps,
|
|
2705
|
+
customCertPath,
|
|
2706
|
+
customKeyPath,
|
|
2707
|
+
lanMode,
|
|
2708
|
+
lanIp: desiredConfig.lanIpExplicit ? lanIp : null,
|
|
2709
|
+
lanIpExplicit: desiredConfig.lanIpExplicit,
|
|
2710
|
+
tld,
|
|
2711
|
+
useWildcard: desiredWildcard
|
|
2712
|
+
};
|
|
1842
2713
|
if (!isWindows && proxyPort < PRIVILEGED_PORT_THRESHOLD && (process.getuid?.() ?? -1) !== 0) {
|
|
1843
|
-
const
|
|
2714
|
+
const startArgs = [
|
|
1844
2715
|
process.execPath,
|
|
1845
2716
|
getEntryScript(),
|
|
1846
2717
|
"proxy",
|
|
1847
2718
|
"start",
|
|
1848
|
-
|
|
1849
|
-
|
|
2719
|
+
...buildProxyStartConfig({
|
|
2720
|
+
useHttps,
|
|
2721
|
+
customCertPath,
|
|
2722
|
+
customKeyPath,
|
|
2723
|
+
lanMode,
|
|
2724
|
+
lanIp: desiredConfig.lanIpExplicit ? lanIp : null,
|
|
2725
|
+
lanIpExplicit: desiredConfig.lanIpExplicit,
|
|
2726
|
+
tld,
|
|
2727
|
+
useWildcard: desiredWildcard,
|
|
2728
|
+
foreground: isForeground,
|
|
2729
|
+
includePort: true,
|
|
2730
|
+
proxyPort
|
|
2731
|
+
}).args
|
|
1850
2732
|
];
|
|
1851
|
-
const
|
|
1852
|
-
|
|
1853
|
-
if (tld !== DEFAULT_TLD) optionalFlags.push("--tld", tld);
|
|
1854
|
-
if (useWildcard) optionalFlags.push("--wildcard");
|
|
1855
|
-
if (isForeground) optionalFlags.push("--foreground");
|
|
1856
|
-
if (customCertPath && customKeyPath)
|
|
1857
|
-
optionalFlags.push("--cert", customCertPath, "--key", customKeyPath);
|
|
1858
|
-
const startArgs = [...baseArgs, ...optionalFlags];
|
|
1859
|
-
const extraFlags = optionalFlags.map((a) => ` ${a}`).join("");
|
|
2733
|
+
const fallbackCommand = formatProxyStartCommand(FALLBACK_PROXY_PORT, resolvedConfig);
|
|
2734
|
+
const currentCommand = formatProxyStartCommand(proxyPort, resolvedConfig);
|
|
1860
2735
|
console.log(
|
|
1861
2736
|
colors_default.yellow(`Port ${proxyPort} requires elevated privileges. Requesting sudo...`)
|
|
1862
2737
|
);
|
|
1863
2738
|
if (!hasExplicitPort) {
|
|
1864
|
-
console.log(
|
|
1865
|
-
colors_default.gray(
|
|
1866
|
-
`(To skip sudo, use an unprivileged port: portless proxy start -p ${FALLBACK_PROXY_PORT}${extraFlags})`
|
|
1867
|
-
)
|
|
1868
|
-
);
|
|
2739
|
+
console.log(colors_default.gray(`(To skip sudo, use an unprivileged port: ${fallbackCommand})`));
|
|
1869
2740
|
}
|
|
1870
|
-
const result =
|
|
2741
|
+
const result = spawnSync2("sudo", ["env", ...collectPortlessEnvArgs(), ...startArgs], {
|
|
1871
2742
|
stdio: "inherit",
|
|
1872
2743
|
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
1873
2744
|
});
|
|
@@ -1877,8 +2748,8 @@ ${colors_default.bold("Usage:")}
|
|
|
1877
2748
|
console.log(colors_default.green(`Proxy started on port ${proxyPort}.`));
|
|
1878
2749
|
} else {
|
|
1879
2750
|
console.error(colors_default.red("Proxy process started but is not responding."));
|
|
1880
|
-
const logPath2 =
|
|
1881
|
-
if (
|
|
2751
|
+
const logPath2 = path4.join(resolveStateDir(proxyPort), "proxy.log");
|
|
2752
|
+
if (fs4.existsSync(logPath2)) {
|
|
1882
2753
|
console.error(colors_default.gray(`Logs: ${logPath2}`));
|
|
1883
2754
|
}
|
|
1884
2755
|
}
|
|
@@ -1894,7 +2765,7 @@ ${colors_default.bold("Usage:")}
|
|
|
1894
2765
|
console.log(
|
|
1895
2766
|
colors_default.blue(`For clean URLs without port numbers, re-run and accept the sudo prompt:`)
|
|
1896
2767
|
);
|
|
1897
|
-
console.log(colors_default.cyan(`
|
|
2768
|
+
console.log(colors_default.cyan(` ${fallbackCommand}`));
|
|
1898
2769
|
if (await isProxyRunning(proxyPort)) {
|
|
1899
2770
|
console.log(colors_default.yellow(`Proxy is already running on port ${proxyPort}.`));
|
|
1900
2771
|
return;
|
|
@@ -1908,7 +2779,7 @@ ${colors_default.bold("Usage:")}
|
|
|
1908
2779
|
colors_default.red(`Error: Port ${proxyPort} requires elevated privileges and sudo failed.`)
|
|
1909
2780
|
);
|
|
1910
2781
|
console.error(colors_default.blue("Try again (portless will prompt for sudo):"));
|
|
1911
|
-
console.error(colors_default.cyan(`
|
|
2782
|
+
console.error(colors_default.cyan(` ${currentCommand}`));
|
|
1912
2783
|
process.exit(1);
|
|
1913
2784
|
}
|
|
1914
2785
|
}
|
|
@@ -1917,8 +2788,8 @@ ${colors_default.bold("Usage:")}
|
|
|
1917
2788
|
store.ensureDir();
|
|
1918
2789
|
if (customCertPath && customKeyPath) {
|
|
1919
2790
|
try {
|
|
1920
|
-
const cert =
|
|
1921
|
-
const key =
|
|
2791
|
+
const cert = fs4.readFileSync(customCertPath);
|
|
2792
|
+
const key = fs4.readFileSync(customKeyPath);
|
|
1922
2793
|
const certStr = cert.toString("utf-8");
|
|
1923
2794
|
const keyStr = key.toString("utf-8");
|
|
1924
2795
|
if (!certStr.includes("-----BEGIN CERTIFICATE-----")) {
|
|
@@ -1963,9 +2834,9 @@ ${colors_default.bold("Usage:")}
|
|
|
1963
2834
|
console.warn(colors_default.cyan(" portless trust"));
|
|
1964
2835
|
}
|
|
1965
2836
|
}
|
|
1966
|
-
const cert =
|
|
1967
|
-
const key =
|
|
1968
|
-
const ca =
|
|
2837
|
+
const cert = fs4.readFileSync(certs.certPath);
|
|
2838
|
+
const key = fs4.readFileSync(certs.keyPath);
|
|
2839
|
+
const ca = fs4.readFileSync(certs.caPath);
|
|
1969
2840
|
tlsOptions = {
|
|
1970
2841
|
cert,
|
|
1971
2842
|
key,
|
|
@@ -1975,16 +2846,16 @@ ${colors_default.bold("Usage:")}
|
|
|
1975
2846
|
}
|
|
1976
2847
|
}
|
|
1977
2848
|
if (isForeground) {
|
|
1978
|
-
console.log(
|
|
1979
|
-
startProxyServer(store, proxyPort, tld, tlsOptions,
|
|
2849
|
+
console.log(chalk.blue.bold("\nportless proxy\n"));
|
|
2850
|
+
startProxyServer(store, proxyPort, tld, tlsOptions, lanIp, desiredWildcard ? false : void 0);
|
|
1980
2851
|
return;
|
|
1981
2852
|
}
|
|
1982
2853
|
store.ensureDir();
|
|
1983
|
-
const logPath =
|
|
1984
|
-
const logFd =
|
|
2854
|
+
const logPath = path4.join(stateDir, "proxy.log");
|
|
2855
|
+
const logFd = fs4.openSync(logPath, "a");
|
|
1985
2856
|
try {
|
|
1986
2857
|
try {
|
|
1987
|
-
|
|
2858
|
+
fs4.chmodSync(logPath, FILE_MODE);
|
|
1988
2859
|
} catch {
|
|
1989
2860
|
}
|
|
1990
2861
|
fixOwnership(logPath);
|
|
@@ -1992,26 +2863,21 @@ ${colors_default.bold("Usage:")}
|
|
|
1992
2863
|
getEntryScript(),
|
|
1993
2864
|
"proxy",
|
|
1994
2865
|
"start",
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
2866
|
+
...buildProxyStartConfig({
|
|
2867
|
+
useHttps,
|
|
2868
|
+
customCertPath,
|
|
2869
|
+
customKeyPath,
|
|
2870
|
+
lanMode,
|
|
2871
|
+
lanIp: desiredConfig.lanIpExplicit ? lanIp : null,
|
|
2872
|
+
lanIpExplicit: desiredConfig.lanIpExplicit,
|
|
2873
|
+
tld,
|
|
2874
|
+
useWildcard: desiredWildcard,
|
|
2875
|
+
foreground: true,
|
|
2876
|
+
includePort: true,
|
|
2877
|
+
proxyPort
|
|
2878
|
+
}).args
|
|
1998
2879
|
];
|
|
1999
|
-
|
|
2000
|
-
if (customCertPath && customKeyPath) {
|
|
2001
|
-
daemonArgs.push("--cert", customCertPath, "--key", customKeyPath);
|
|
2002
|
-
} else {
|
|
2003
|
-
daemonArgs.push("--https");
|
|
2004
|
-
}
|
|
2005
|
-
} else {
|
|
2006
|
-
daemonArgs.push("--no-tls");
|
|
2007
|
-
}
|
|
2008
|
-
if (tld !== DEFAULT_TLD) {
|
|
2009
|
-
daemonArgs.push("--tld", tld);
|
|
2010
|
-
}
|
|
2011
|
-
if (useWildcard) {
|
|
2012
|
-
daemonArgs.push("--wildcard");
|
|
2013
|
-
}
|
|
2014
|
-
const child = spawn(process.execPath, daemonArgs, {
|
|
2880
|
+
const child = spawn2(process.execPath, daemonArgs, {
|
|
2015
2881
|
detached: true,
|
|
2016
2882
|
stdio: ["ignore", logFd, logFd],
|
|
2017
2883
|
env: process.env,
|
|
@@ -2019,19 +2885,23 @@ ${colors_default.bold("Usage:")}
|
|
|
2019
2885
|
});
|
|
2020
2886
|
child.unref();
|
|
2021
2887
|
} finally {
|
|
2022
|
-
|
|
2888
|
+
fs4.closeSync(logFd);
|
|
2023
2889
|
}
|
|
2024
2890
|
if (!await waitForProxy(proxyPort, void 0, void 0, useHttps)) {
|
|
2025
2891
|
console.error(colors_default.red("Proxy failed to start (timed out waiting for it to listen)."));
|
|
2026
2892
|
console.error(colors_default.blue("Try starting the proxy in the foreground to see the error:"));
|
|
2027
2893
|
console.error(colors_default.cyan(" portless proxy start --foreground"));
|
|
2028
|
-
if (
|
|
2894
|
+
if (fs4.existsSync(logPath)) {
|
|
2029
2895
|
console.error(colors_default.gray(`Logs: ${logPath}`));
|
|
2030
2896
|
}
|
|
2031
2897
|
process.exit(1);
|
|
2032
2898
|
}
|
|
2033
2899
|
const proto = useHttps ? "HTTPS/2" : "HTTP";
|
|
2034
|
-
console.log(
|
|
2900
|
+
console.log(chalk.green(`${proto} proxy started on port ${proxyPort}`));
|
|
2901
|
+
if (lanMode && lanIp) {
|
|
2902
|
+
console.log(chalk.green(`LAN mode active. IP: ${lanIp}`));
|
|
2903
|
+
console.log(chalk.gray("Services will be discoverable as <name>.local on your network."));
|
|
2904
|
+
}
|
|
2035
2905
|
}
|
|
2036
2906
|
async function handleRunMode(args) {
|
|
2037
2907
|
const parsed = parseRunArgs(args);
|
|
@@ -2055,7 +2925,7 @@ async function handleRunMode(args) {
|
|
|
2055
2925
|
}
|
|
2056
2926
|
const worktree = detectWorktreePrefix();
|
|
2057
2927
|
const effectiveName = worktree ? `${worktree.prefix}.${baseName}` : baseName;
|
|
2058
|
-
const { dir, port, tls: tls2, tld } = await discoverState();
|
|
2928
|
+
const { dir, port, tls: tls2, tld, lanMode, lanIp } = await discoverState();
|
|
2059
2929
|
const store = new RouteStore(dir, {
|
|
2060
2930
|
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
2061
2931
|
});
|
|
@@ -2069,7 +2939,9 @@ async function handleRunMode(args) {
|
|
|
2069
2939
|
tld,
|
|
2070
2940
|
parsed.force,
|
|
2071
2941
|
{ nameSource, prefix: worktree?.prefix, prefixSource: worktree?.source },
|
|
2072
|
-
parsed.appPort
|
|
2942
|
+
parsed.appPort,
|
|
2943
|
+
lanMode,
|
|
2944
|
+
lanIp
|
|
2073
2945
|
);
|
|
2074
2946
|
}
|
|
2075
2947
|
async function handleNamedMode(args) {
|
|
@@ -2083,7 +2955,7 @@ async function handleNamedMode(args) {
|
|
|
2083
2955
|
process.exit(1);
|
|
2084
2956
|
}
|
|
2085
2957
|
const safeName = parsed.name.split(".").map((label) => truncateLabel(label)).join(".");
|
|
2086
|
-
const { dir, port, tls: tls2, tld } = await discoverState();
|
|
2958
|
+
const { dir, port, tls: tls2, tld, lanMode, lanIp } = await discoverState();
|
|
2087
2959
|
const store = new RouteStore(dir, {
|
|
2088
2960
|
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
2089
2961
|
});
|
|
@@ -2097,7 +2969,9 @@ async function handleNamedMode(args) {
|
|
|
2097
2969
|
tld,
|
|
2098
2970
|
parsed.force,
|
|
2099
2971
|
void 0,
|
|
2100
|
-
parsed.appPort
|
|
2972
|
+
parsed.appPort,
|
|
2973
|
+
lanMode,
|
|
2974
|
+
lanIp
|
|
2101
2975
|
);
|
|
2102
2976
|
}
|
|
2103
2977
|
async function main() {
|
|
@@ -2119,6 +2993,40 @@ async function main() {
|
|
|
2119
2993
|
console.error(colors_default.cyan(" npm install -D portless"));
|
|
2120
2994
|
process.exit(1);
|
|
2121
2995
|
}
|
|
2996
|
+
const stripGlobalFlag = (flag, hasValue) => {
|
|
2997
|
+
const sep = args.indexOf("--");
|
|
2998
|
+
const end = sep === -1 ? args.length : sep;
|
|
2999
|
+
const idx = args.indexOf(flag);
|
|
3000
|
+
if (idx === -1 || idx >= end) return null;
|
|
3001
|
+
if (!hasValue) {
|
|
3002
|
+
args.splice(idx, 1);
|
|
3003
|
+
return true;
|
|
3004
|
+
}
|
|
3005
|
+
const value = args[idx + 1];
|
|
3006
|
+
if (!value || value.startsWith("-")) return false;
|
|
3007
|
+
args.splice(idx, 2);
|
|
3008
|
+
return value;
|
|
3009
|
+
};
|
|
3010
|
+
if (stripGlobalFlag("--lan", false)) {
|
|
3011
|
+
process.env.PORTLESS_LAN = "1";
|
|
3012
|
+
}
|
|
3013
|
+
const ipResult = stripGlobalFlag("--ip", true);
|
|
3014
|
+
if (ipResult === false) {
|
|
3015
|
+
console.error(chalk.red("Error: --ip requires an IP address."));
|
|
3016
|
+
console.error(chalk.cyan(" portless --lan --ip 192.168.1.42 run <command>"));
|
|
3017
|
+
process.exit(1);
|
|
3018
|
+
} else if (typeof ipResult === "string") {
|
|
3019
|
+
process.env.PORTLESS_LAN_IP = ipResult;
|
|
3020
|
+
process.env.PORTLESS_LAN = "1";
|
|
3021
|
+
}
|
|
3022
|
+
const autoIpResult = stripGlobalFlag(INTERNAL_LAN_IP_FLAG, true);
|
|
3023
|
+
if (autoIpResult === false) {
|
|
3024
|
+
console.error(chalk.red(`Error: ${INTERNAL_LAN_IP_FLAG} requires an IP address.`));
|
|
3025
|
+
process.exit(1);
|
|
3026
|
+
} else if (typeof autoIpResult === "string") {
|
|
3027
|
+
process.env[INTERNAL_LAN_IP_ENV] = autoIpResult;
|
|
3028
|
+
process.env.PORTLESS_LAN = "1";
|
|
3029
|
+
}
|
|
2122
3030
|
if (args[0] === "--name") {
|
|
2123
3031
|
args.shift();
|
|
2124
3032
|
if (!args[0]) {
|
|
@@ -2144,7 +3052,7 @@ async function main() {
|
|
|
2144
3052
|
args.shift();
|
|
2145
3053
|
}
|
|
2146
3054
|
const skipPortless = process.env.PORTLESS === "0" || process.env.PORTLESS === "false" || process.env.PORTLESS === "skip";
|
|
2147
|
-
if (skipPortless && (isRunCommand || args.length >= 2 && args[0] !== "proxy")) {
|
|
3055
|
+
if (skipPortless && (isRunCommand || args.length >= 2 && args[0] !== "proxy" && args[0] !== "clean")) {
|
|
2148
3056
|
const { commandArgs } = isRunCommand ? parseRunArgs(args) : parseAppArgs(args);
|
|
2149
3057
|
if (commandArgs.length === 0) {
|
|
2150
3058
|
console.error(colors_default.red("Error: No command provided."));
|
|
@@ -2166,6 +3074,10 @@ async function main() {
|
|
|
2166
3074
|
await handleTrust();
|
|
2167
3075
|
return;
|
|
2168
3076
|
}
|
|
3077
|
+
if (args[0] === "clean") {
|
|
3078
|
+
await handleClean(args);
|
|
3079
|
+
return;
|
|
3080
|
+
}
|
|
2169
3081
|
if (args[0] === "list") {
|
|
2170
3082
|
await handleList();
|
|
2171
3083
|
return;
|