portless 0.9.6 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -3
- package/dist/{chunk-OPTRASOS.js → chunk-WXEE5QH6.js} +133 -14
- package/dist/cli.js +757 -106
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3,12 +3,15 @@ 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,
|
|
10
12
|
WAIT_FOR_PROXY_INTERVAL_MS,
|
|
11
13
|
WAIT_FOR_PROXY_MAX_ATTEMPTS,
|
|
14
|
+
buildProxyStartConfig,
|
|
12
15
|
cleanHostsFile,
|
|
13
16
|
createHttpRedirectServer,
|
|
14
17
|
createProxyServer,
|
|
@@ -19,23 +22,28 @@ import {
|
|
|
19
22
|
formatUrl,
|
|
20
23
|
getDefaultPort,
|
|
21
24
|
getDefaultTld,
|
|
22
|
-
getProtocolPort,
|
|
23
25
|
injectFrameworkFlags,
|
|
24
26
|
isErrnoException,
|
|
25
27
|
isHttpsEnvDisabled,
|
|
28
|
+
isLanEnvEnabled,
|
|
29
|
+
isPortListening,
|
|
26
30
|
isProxyRunning,
|
|
27
31
|
isWildcardEnvEnabled,
|
|
28
32
|
isWindows,
|
|
29
33
|
parseHostname,
|
|
30
34
|
prompt,
|
|
35
|
+
readLanMarker,
|
|
36
|
+
readTldFromDir,
|
|
37
|
+
readTlsMarker,
|
|
31
38
|
resolveStateDir,
|
|
32
39
|
spawnCommand,
|
|
33
40
|
syncHostsFile,
|
|
34
41
|
validateTld,
|
|
35
42
|
waitForProxy,
|
|
43
|
+
writeLanMarker,
|
|
36
44
|
writeTldFile,
|
|
37
45
|
writeTlsMarker
|
|
38
|
-
} from "./chunk-
|
|
46
|
+
} from "./chunk-WXEE5QH6.js";
|
|
39
47
|
|
|
40
48
|
// src/colors.ts
|
|
41
49
|
function supportsColor() {
|
|
@@ -65,7 +73,7 @@ var colors_default = { bold, red, green, yellow, blue, cyan, white, gray };
|
|
|
65
73
|
// src/cli.ts
|
|
66
74
|
import * as fs3 from "fs";
|
|
67
75
|
import * as path3 from "path";
|
|
68
|
-
import { spawn, spawnSync } from "child_process";
|
|
76
|
+
import { spawn as spawn2, spawnSync as spawnSync2 } from "child_process";
|
|
69
77
|
|
|
70
78
|
// src/certs.ts
|
|
71
79
|
import * as fs from "fs";
|
|
@@ -135,6 +143,14 @@ function isCertValid(certPath) {
|
|
|
135
143
|
return false;
|
|
136
144
|
}
|
|
137
145
|
}
|
|
146
|
+
function isCertSansComplete(certPath) {
|
|
147
|
+
try {
|
|
148
|
+
const text = openssl(["x509", "-in", certPath, "-noout", "-text"]);
|
|
149
|
+
return /DNS:\*\.local\b/.test(text);
|
|
150
|
+
} catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
138
154
|
function isCertSignatureStrong(certPath) {
|
|
139
155
|
try {
|
|
140
156
|
const text = openssl(["x509", "-in", certPath, "-noout", "-text"]);
|
|
@@ -216,6 +232,7 @@ function generateServerCert(stateDir) {
|
|
|
216
232
|
const extPath = path.join(stateDir, "server-ext.cnf");
|
|
217
233
|
openssl(["ecparam", "-genkey", "-name", "prime256v1", "-noout", "-out", serverKeyPath]);
|
|
218
234
|
openssl(["req", "-new", "-key", serverKeyPath, "-out", csrPath, "-subj", "/CN=localhost"]);
|
|
235
|
+
const sans = ["DNS:localhost", "DNS:*.localhost", "DNS:*.local"];
|
|
219
236
|
fs.writeFileSync(
|
|
220
237
|
extPath,
|
|
221
238
|
[
|
|
@@ -223,7 +240,7 @@ function generateServerCert(stateDir) {
|
|
|
223
240
|
"basicConstraints=CA:FALSE",
|
|
224
241
|
"keyUsage=digitalSignature,keyEncipherment",
|
|
225
242
|
"extendedKeyUsage=serverAuth",
|
|
226
|
-
"
|
|
243
|
+
`subjectAltName=${sans.join(",")}`
|
|
227
244
|
].join("\n") + "\n"
|
|
228
245
|
);
|
|
229
246
|
const srlPath = path.join(stateDir, "ca.srl");
|
|
@@ -273,7 +290,7 @@ function ensureCerts(stateDir) {
|
|
|
273
290
|
generateCA(stateDir);
|
|
274
291
|
caGenerated = true;
|
|
275
292
|
}
|
|
276
|
-
if (caGenerated || !fileExists(serverCertPath) || !fileExists(serverKeyPath) || !isCertValid(serverCertPath) || !isCertSignatureStrong(serverCertPath)) {
|
|
293
|
+
if (caGenerated || !fileExists(serverCertPath) || !fileExists(serverKeyPath) || !isCertValid(serverCertPath) || !isCertSignatureStrong(serverCertPath) || !isCertSansComplete(serverCertPath)) {
|
|
277
294
|
generateServerCert(stateDir);
|
|
278
295
|
}
|
|
279
296
|
return {
|
|
@@ -766,12 +783,351 @@ function readBranchFromHead(gitdir) {
|
|
|
766
783
|
}
|
|
767
784
|
}
|
|
768
785
|
|
|
786
|
+
// src/mdns.ts
|
|
787
|
+
import { spawn, spawnSync } from "child_process";
|
|
788
|
+
|
|
789
|
+
// src/lan-ip.ts
|
|
790
|
+
import { createSocket } from "dgram";
|
|
791
|
+
import { networkInterfaces } from "os";
|
|
792
|
+
var PROBE_HOST = "1.1.1.1";
|
|
793
|
+
var PROBE_PORT = 53;
|
|
794
|
+
var NO_ROUTE_IP = "0.0.0.0";
|
|
795
|
+
function isIPv4Family(family) {
|
|
796
|
+
return family === "IPv4" || family === 4;
|
|
797
|
+
}
|
|
798
|
+
function parseMac(macStr) {
|
|
799
|
+
return macStr.split(":").slice(0, 16).map((seq) => parseInt(seq, 16));
|
|
800
|
+
}
|
|
801
|
+
function isInternalInterface(iname, macStr, internal) {
|
|
802
|
+
if (internal) {
|
|
803
|
+
return true;
|
|
804
|
+
}
|
|
805
|
+
const mac = parseMac(macStr);
|
|
806
|
+
if (mac.every((x) => !x)) {
|
|
807
|
+
return true;
|
|
808
|
+
}
|
|
809
|
+
if (mac[0] === 0 && mac[1] === 21 && mac[2] === 93) {
|
|
810
|
+
return true;
|
|
811
|
+
}
|
|
812
|
+
if (iname.includes("vEthernet") || /^bridge\d+$/.test(iname)) {
|
|
813
|
+
return true;
|
|
814
|
+
}
|
|
815
|
+
return false;
|
|
816
|
+
}
|
|
817
|
+
function probeDefaultRouteIPv4() {
|
|
818
|
+
return new Promise((resolve2, reject) => {
|
|
819
|
+
const socket = createSocket({ type: "udp4", reuseAddr: true });
|
|
820
|
+
socket.on("error", (error) => {
|
|
821
|
+
socket.close();
|
|
822
|
+
socket.unref();
|
|
823
|
+
reject(error);
|
|
824
|
+
});
|
|
825
|
+
socket.connect(PROBE_PORT, PROBE_HOST, () => {
|
|
826
|
+
const addr = socket.address();
|
|
827
|
+
socket.close();
|
|
828
|
+
socket.unref();
|
|
829
|
+
if (addr && "address" in addr && addr.address && addr.address !== NO_ROUTE_IP) {
|
|
830
|
+
resolve2(addr.address);
|
|
831
|
+
} else {
|
|
832
|
+
reject(new Error("No route to host"));
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
function findInterfaceRowForIp(ip) {
|
|
838
|
+
const ifs = networkInterfaces();
|
|
839
|
+
for (const iname of Object.keys(ifs)) {
|
|
840
|
+
const entries = ifs[iname];
|
|
841
|
+
if (!entries) continue;
|
|
842
|
+
for (const e of entries) {
|
|
843
|
+
if (!isIPv4Family(e.family)) continue;
|
|
844
|
+
if (e.address !== ip) continue;
|
|
845
|
+
return { iname, address: e.address, mac: e.mac, internal: e.internal };
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
return null;
|
|
849
|
+
}
|
|
850
|
+
async function getLocalNetworkIp() {
|
|
851
|
+
try {
|
|
852
|
+
const ip = await probeDefaultRouteIPv4();
|
|
853
|
+
if (ip === "127.0.0.1") {
|
|
854
|
+
return null;
|
|
855
|
+
}
|
|
856
|
+
const row = findInterfaceRowForIp(ip);
|
|
857
|
+
if (!row) {
|
|
858
|
+
return null;
|
|
859
|
+
}
|
|
860
|
+
if (row.address === "127.0.0.1") {
|
|
861
|
+
return null;
|
|
862
|
+
}
|
|
863
|
+
if (isInternalInterface(row.iname, row.mac, row.internal)) {
|
|
864
|
+
return null;
|
|
865
|
+
}
|
|
866
|
+
return row.address;
|
|
867
|
+
} catch {
|
|
868
|
+
return null;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// src/mdns.ts
|
|
873
|
+
var activePublishers = /* @__PURE__ */ new Map();
|
|
874
|
+
var LAN_IP_POLL_INTERVAL_MS = 5e3;
|
|
875
|
+
function getMdnsPublisher() {
|
|
876
|
+
if (process.platform === "darwin") {
|
|
877
|
+
return {
|
|
878
|
+
command: "dns-sd",
|
|
879
|
+
probeArgs: ["-h"],
|
|
880
|
+
missingReason: "dns-sd not found",
|
|
881
|
+
buildArgs: (fqdn, name, port, ip) => [
|
|
882
|
+
"-P",
|
|
883
|
+
name,
|
|
884
|
+
"_http._tcp",
|
|
885
|
+
"local",
|
|
886
|
+
port.toString(),
|
|
887
|
+
fqdn,
|
|
888
|
+
ip
|
|
889
|
+
]
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
if (process.platform === "linux") {
|
|
893
|
+
return {
|
|
894
|
+
command: "avahi-publish-address",
|
|
895
|
+
probeArgs: ["--help"],
|
|
896
|
+
missingReason: "avahi-publish-address not found. Install avahi-utils: sudo apt install avahi-utils",
|
|
897
|
+
buildArgs: (fqdn, _name, _port, ip) => ["-R", fqdn, ip]
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
return null;
|
|
901
|
+
}
|
|
902
|
+
function hasCommand(command, probeArgs) {
|
|
903
|
+
const result = spawnSync(command, probeArgs, {
|
|
904
|
+
stdio: "ignore",
|
|
905
|
+
timeout: 1e3,
|
|
906
|
+
windowsHide: true
|
|
907
|
+
});
|
|
908
|
+
return result.error?.code !== "ENOENT";
|
|
909
|
+
}
|
|
910
|
+
function startLanIpMonitor(options) {
|
|
911
|
+
const resolveIp = options.resolveIp ?? getLocalNetworkIp;
|
|
912
|
+
let currentIp = options.initialIp;
|
|
913
|
+
let stopped = false;
|
|
914
|
+
let polling = false;
|
|
915
|
+
const poll = async () => {
|
|
916
|
+
if (stopped || polling) return;
|
|
917
|
+
polling = true;
|
|
918
|
+
try {
|
|
919
|
+
const nextIp = await resolveIp();
|
|
920
|
+
if (stopped || nextIp === currentIp) return;
|
|
921
|
+
const previousIp = currentIp;
|
|
922
|
+
currentIp = nextIp;
|
|
923
|
+
options.onChange(nextIp, previousIp);
|
|
924
|
+
} catch (error) {
|
|
925
|
+
options.onError?.(error);
|
|
926
|
+
} finally {
|
|
927
|
+
polling = false;
|
|
928
|
+
}
|
|
929
|
+
};
|
|
930
|
+
const timer = setInterval(() => {
|
|
931
|
+
void poll();
|
|
932
|
+
}, options.intervalMs ?? LAN_IP_POLL_INTERVAL_MS);
|
|
933
|
+
timer.unref?.();
|
|
934
|
+
return {
|
|
935
|
+
stop: () => {
|
|
936
|
+
stopped = true;
|
|
937
|
+
clearInterval(timer);
|
|
938
|
+
}
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
function isMdnsSupported() {
|
|
942
|
+
const publisher = getMdnsPublisher();
|
|
943
|
+
if (!publisher) {
|
|
944
|
+
return { supported: false, reason: "mDNS publishing is not supported on this platform" };
|
|
945
|
+
}
|
|
946
|
+
if (!hasCommand(publisher.command, publisher.probeArgs)) {
|
|
947
|
+
return { supported: false, reason: publisher.missingReason };
|
|
948
|
+
}
|
|
949
|
+
return { supported: true };
|
|
950
|
+
}
|
|
951
|
+
function serviceName(hostname) {
|
|
952
|
+
return hostname.replace(/\.local$/, "");
|
|
953
|
+
}
|
|
954
|
+
function publish(hostname, port, ip, onError) {
|
|
955
|
+
if (activePublishers.has(hostname)) return;
|
|
956
|
+
const fqdn = hostname.endsWith(".local") ? hostname : `${hostname}.local`;
|
|
957
|
+
const name = serviceName(fqdn);
|
|
958
|
+
const publisher = getMdnsPublisher();
|
|
959
|
+
if (!publisher) {
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
const child = spawn(publisher.command, publisher.buildArgs(fqdn, name, port, ip), {
|
|
963
|
+
stdio: "ignore",
|
|
964
|
+
detached: false
|
|
965
|
+
});
|
|
966
|
+
child.on("error", (err) => {
|
|
967
|
+
activePublishers.delete(hostname);
|
|
968
|
+
const msg = err.code === "ENOENT" ? publisher.missingReason : `mDNS publish error for ${hostname}: ${err.message}`;
|
|
969
|
+
onError?.(msg);
|
|
970
|
+
});
|
|
971
|
+
child.on("exit", () => {
|
|
972
|
+
activePublishers.delete(hostname);
|
|
973
|
+
});
|
|
974
|
+
activePublishers.set(hostname, child);
|
|
975
|
+
}
|
|
976
|
+
function unpublish(hostname) {
|
|
977
|
+
const child = activePublishers.get(hostname);
|
|
978
|
+
if (!child) return;
|
|
979
|
+
activePublishers.delete(hostname);
|
|
980
|
+
child.kill("SIGTERM");
|
|
981
|
+
}
|
|
982
|
+
function cleanupAll() {
|
|
983
|
+
for (const child of activePublishers.values()) {
|
|
984
|
+
child.kill("SIGTERM");
|
|
985
|
+
}
|
|
986
|
+
activePublishers.clear();
|
|
987
|
+
}
|
|
988
|
+
|
|
769
989
|
// src/cli.ts
|
|
990
|
+
var chalk = colors_default;
|
|
770
991
|
var HOSTS_DISPLAY = isWindows ? "hosts file" : "/etc/hosts";
|
|
771
992
|
var DEBOUNCE_MS = 100;
|
|
772
993
|
var POLL_INTERVAL_MS = 3e3;
|
|
773
994
|
var EXIT_TIMEOUT_MS = 2e3;
|
|
774
995
|
var SUDO_SPAWN_TIMEOUT_MS = 3e4;
|
|
996
|
+
function defaultProxyConfig(tld, useHttps, lanMode) {
|
|
997
|
+
return {
|
|
998
|
+
useHttps,
|
|
999
|
+
customCertPath: null,
|
|
1000
|
+
customKeyPath: null,
|
|
1001
|
+
lanMode,
|
|
1002
|
+
lanIp: null,
|
|
1003
|
+
lanIpExplicit: false,
|
|
1004
|
+
tld: lanMode ? "local" : tld,
|
|
1005
|
+
useWildcard: false
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
function resolveProxyConfig(options) {
|
|
1009
|
+
const config = defaultProxyConfig(
|
|
1010
|
+
options.defaultTld,
|
|
1011
|
+
options.useHttps,
|
|
1012
|
+
options.explicit.lanMode ? options.lanMode : options.persistedLanMode
|
|
1013
|
+
);
|
|
1014
|
+
if (options.explicit.useHttps) {
|
|
1015
|
+
config.useHttps = options.useHttps;
|
|
1016
|
+
if (!options.useHttps) {
|
|
1017
|
+
config.customCertPath = null;
|
|
1018
|
+
config.customKeyPath = null;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
if (options.explicit.customCert) {
|
|
1022
|
+
config.useHttps = true;
|
|
1023
|
+
config.customCertPath = options.customCertPath;
|
|
1024
|
+
config.customKeyPath = options.customKeyPath;
|
|
1025
|
+
}
|
|
1026
|
+
if (options.explicit.lanMode) {
|
|
1027
|
+
config.lanMode = options.lanMode;
|
|
1028
|
+
if (!options.lanMode) {
|
|
1029
|
+
config.lanIp = null;
|
|
1030
|
+
config.lanIpExplicit = false;
|
|
1031
|
+
if (!options.explicit.tld) {
|
|
1032
|
+
config.tld = options.defaultTld;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
if (options.explicit.lanIp && options.lanIp) {
|
|
1037
|
+
config.lanMode = true;
|
|
1038
|
+
config.lanIp = options.lanIp;
|
|
1039
|
+
config.lanIpExplicit = true;
|
|
1040
|
+
}
|
|
1041
|
+
if (options.explicit.tld) {
|
|
1042
|
+
config.tld = options.tld;
|
|
1043
|
+
}
|
|
1044
|
+
if (options.explicit.useWildcard) {
|
|
1045
|
+
config.useWildcard = options.useWildcard;
|
|
1046
|
+
}
|
|
1047
|
+
if (!config.lanMode) {
|
|
1048
|
+
config.lanIp = null;
|
|
1049
|
+
config.lanIpExplicit = false;
|
|
1050
|
+
}
|
|
1051
|
+
if (config.lanMode) {
|
|
1052
|
+
config.tld = "local";
|
|
1053
|
+
if (!config.lanIpExplicit) {
|
|
1054
|
+
config.lanIp = null;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
if (!config.useHttps) {
|
|
1058
|
+
config.customCertPath = null;
|
|
1059
|
+
config.customKeyPath = null;
|
|
1060
|
+
}
|
|
1061
|
+
return config;
|
|
1062
|
+
}
|
|
1063
|
+
function readCurrentProxyConfig(dir) {
|
|
1064
|
+
const lanIp = readLanMarker(dir);
|
|
1065
|
+
const tld = readTldFromDir(dir);
|
|
1066
|
+
return {
|
|
1067
|
+
useHttps: readTlsMarker(dir),
|
|
1068
|
+
customCertPath: null,
|
|
1069
|
+
customKeyPath: null,
|
|
1070
|
+
lanMode: lanIp !== null || tld === "local",
|
|
1071
|
+
lanIp,
|
|
1072
|
+
lanIpExplicit: false,
|
|
1073
|
+
tld,
|
|
1074
|
+
useWildcard: false
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
function getProxyConfigMismatchMessages(desiredConfig, actualConfig, explicit) {
|
|
1078
|
+
const messages = [];
|
|
1079
|
+
if (explicit.lanMode && desiredConfig.lanMode !== actualConfig.lanMode) {
|
|
1080
|
+
messages.push(
|
|
1081
|
+
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"
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
if (explicit.lanIp && desiredConfig.lanIp !== actualConfig.lanIp) {
|
|
1085
|
+
messages.push(
|
|
1086
|
+
`requested LAN IP ${desiredConfig.lanIp}, but the running proxy is using ${actualConfig.lanIp ?? "auto-detected LAN mode"}`
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
if (explicit.useHttps && desiredConfig.useHttps !== actualConfig.useHttps) {
|
|
1090
|
+
messages.push(
|
|
1091
|
+
desiredConfig.useHttps ? "requested HTTPS, but the running proxy is using HTTP" : "requested HTTP, but the running proxy is using HTTPS"
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
if (explicit.tld && desiredConfig.tld !== actualConfig.tld) {
|
|
1095
|
+
messages.push(
|
|
1096
|
+
`requested .${desiredConfig.tld}, but the running proxy is using .${actualConfig.tld}`
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
return messages;
|
|
1100
|
+
}
|
|
1101
|
+
function formatProxyStartCommand(proxyPort, config) {
|
|
1102
|
+
const needsSudo = !isWindows && proxyPort < PRIVILEGED_PORT_THRESHOLD;
|
|
1103
|
+
const { args } = buildProxyStartConfig({
|
|
1104
|
+
useHttps: config.useHttps,
|
|
1105
|
+
customCertPath: config.customCertPath,
|
|
1106
|
+
customKeyPath: config.customKeyPath,
|
|
1107
|
+
lanMode: config.lanMode,
|
|
1108
|
+
lanIp: config.lanIpExplicit ? config.lanIp : null,
|
|
1109
|
+
lanIpExplicit: config.lanIpExplicit,
|
|
1110
|
+
tld: config.tld,
|
|
1111
|
+
useWildcard: config.useWildcard,
|
|
1112
|
+
includePort: proxyPort !== getDefaultPort(config.useHttps),
|
|
1113
|
+
proxyPort
|
|
1114
|
+
});
|
|
1115
|
+
return `${needsSudo ? "sudo " : ""}portless proxy start${args.length > 0 ? ` ${args.join(" ")}` : ""}`;
|
|
1116
|
+
}
|
|
1117
|
+
function printProxyConfigMismatch(proxyPort, desiredConfig, messages) {
|
|
1118
|
+
const needsSudo = !isWindows && proxyPort < PRIVILEGED_PORT_THRESHOLD;
|
|
1119
|
+
const portFlag = proxyPort !== getDefaultPort(desiredConfig.useHttps) ? ` -p ${proxyPort}` : "";
|
|
1120
|
+
console.error(
|
|
1121
|
+
chalk.yellow(`Proxy is already running on port ${proxyPort} with a different config.`)
|
|
1122
|
+
);
|
|
1123
|
+
for (const message of messages) {
|
|
1124
|
+
console.error(chalk.yellow(`- ${message}`));
|
|
1125
|
+
}
|
|
1126
|
+
console.error(chalk.blue("Stop it first, then restart with the desired settings:"));
|
|
1127
|
+
console.error(chalk.cyan(` ${needsSudo ? "sudo " : ""}portless proxy stop${portFlag}`));
|
|
1128
|
+
console.error(chalk.cyan(` ${formatProxyStartCommand(proxyPort, desiredConfig)}`));
|
|
1129
|
+
process.exit(1);
|
|
1130
|
+
}
|
|
775
1131
|
function getEntryScript() {
|
|
776
1132
|
const script = process.argv[1];
|
|
777
1133
|
if (!script) {
|
|
@@ -803,15 +1159,23 @@ function collectPortlessEnvArgs() {
|
|
|
803
1159
|
function sudoStop(port) {
|
|
804
1160
|
const stopArgs = [process.execPath, getEntryScript(), "proxy", "stop", "-p", String(port)];
|
|
805
1161
|
console.log(colors_default.yellow("Proxy is running as root. Elevating with sudo to stop it..."));
|
|
806
|
-
const result =
|
|
1162
|
+
const result = spawnSync2("sudo", ["env", ...collectPortlessEnvArgs(), ...stopArgs], {
|
|
807
1163
|
stdio: "inherit",
|
|
808
1164
|
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
809
1165
|
});
|
|
810
1166
|
return result.status === 0;
|
|
811
1167
|
}
|
|
812
|
-
function startProxyServer(store, proxyPort, tld, tlsOptions, strict) {
|
|
1168
|
+
function startProxyServer(store, proxyPort, tld, tlsOptions, lanIp, strict) {
|
|
813
1169
|
store.ensureDir();
|
|
814
1170
|
const isTls = !!tlsOptions;
|
|
1171
|
+
const mdnsSupport = isMdnsSupported();
|
|
1172
|
+
let activeLanIp = lanIp && mdnsSupport.supported ? lanIp : null;
|
|
1173
|
+
const lanIpPinned = !!process.env.PORTLESS_LAN_IP;
|
|
1174
|
+
let lanMonitor = null;
|
|
1175
|
+
if (lanIp && !mdnsSupport.supported) {
|
|
1176
|
+
const reason = mdnsSupport.reason ?? "mDNS publishing is not supported on this platform.";
|
|
1177
|
+
console.warn(chalk.yellow(`LAN mode disabled: ${reason}`));
|
|
1178
|
+
}
|
|
815
1179
|
const routesPath = store.getRoutesPath();
|
|
816
1180
|
if (!fs3.existsSync(routesPath)) {
|
|
817
1181
|
fs3.writeFileSync(routesPath, "[]", { mode: FILE_MODE });
|
|
@@ -826,13 +1190,54 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, strict) {
|
|
|
826
1190
|
let watcher = null;
|
|
827
1191
|
let pollingInterval = null;
|
|
828
1192
|
const syncVal = process.env.PORTLESS_SYNC_HOSTS;
|
|
829
|
-
const autoSyncHosts = syncVal === "1" || syncVal === "true" || tld !== DEFAULT_TLD && syncVal !== "0" && syncVal !== "false";
|
|
1193
|
+
const autoSyncHosts = syncVal === "1" || syncVal === "true" || tld !== DEFAULT_TLD && !activeLanIp && syncVal !== "0" && syncVal !== "false";
|
|
1194
|
+
const onMdnsError = (msg) => console.warn(chalk.yellow(msg));
|
|
1195
|
+
const publishCachedRoutes = () => {
|
|
1196
|
+
if (!activeLanIp) return;
|
|
1197
|
+
for (const route of cachedRoutes) {
|
|
1198
|
+
publish(route.hostname, proxyPort, activeLanIp, onMdnsError);
|
|
1199
|
+
}
|
|
1200
|
+
};
|
|
1201
|
+
const updateLanIp = (nextIp, previousIp = activeLanIp) => {
|
|
1202
|
+
if (nextIp === activeLanIp) return;
|
|
1203
|
+
if (activeLanIp) {
|
|
1204
|
+
cleanupAll();
|
|
1205
|
+
}
|
|
1206
|
+
activeLanIp = nextIp;
|
|
1207
|
+
writeLanMarker(store.dir, activeLanIp);
|
|
1208
|
+
if (previousIp && nextIp) {
|
|
1209
|
+
console.log(chalk.green(`LAN IP changed: ${previousIp} -> ${nextIp}`));
|
|
1210
|
+
} else if (previousIp && !nextIp) {
|
|
1211
|
+
console.warn(chalk.yellow("LAN mode temporarily unavailable: no active LAN IP"));
|
|
1212
|
+
} else if (!previousIp && nextIp) {
|
|
1213
|
+
console.log(chalk.green(`LAN mode restored: ${nextIp}`));
|
|
1214
|
+
}
|
|
1215
|
+
publishCachedRoutes();
|
|
1216
|
+
};
|
|
830
1217
|
const reloadRoutes = () => {
|
|
831
1218
|
try {
|
|
1219
|
+
const previousRoutes = new Map(cachedRoutes.map((r) => [r.hostname, r.port]));
|
|
832
1220
|
cachedRoutes = store.loadRoutes();
|
|
833
1221
|
if (autoSyncHosts) {
|
|
834
1222
|
syncHostsFile(cachedRoutes.map((r) => r.hostname));
|
|
835
1223
|
}
|
|
1224
|
+
if (activeLanIp) {
|
|
1225
|
+
const currentRoutes = new Map(cachedRoutes.map((r) => [r.hostname, r.port]));
|
|
1226
|
+
for (const route of cachedRoutes) {
|
|
1227
|
+
const previousPort = previousRoutes.get(route.hostname);
|
|
1228
|
+
if (previousPort === void 0) {
|
|
1229
|
+
publish(route.hostname, proxyPort, activeLanIp, onMdnsError);
|
|
1230
|
+
} else if (previousPort !== route.port) {
|
|
1231
|
+
unpublish(route.hostname);
|
|
1232
|
+
publish(route.hostname, proxyPort, activeLanIp, onMdnsError);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
for (const hostname of previousRoutes.keys()) {
|
|
1236
|
+
if (!currentRoutes.has(hostname)) {
|
|
1237
|
+
unpublish(hostname);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
836
1241
|
} catch {
|
|
837
1242
|
}
|
|
838
1243
|
};
|
|
@@ -848,6 +1253,7 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, strict) {
|
|
|
848
1253
|
if (autoSyncHosts) {
|
|
849
1254
|
syncHostsFile(cachedRoutes.map((r) => r.hostname));
|
|
850
1255
|
}
|
|
1256
|
+
publishCachedRoutes();
|
|
851
1257
|
const server = createProxyServer({
|
|
852
1258
|
getRoutes: () => cachedRoutes,
|
|
853
1259
|
proxyPort,
|
|
@@ -890,6 +1296,7 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, strict) {
|
|
|
890
1296
|
fs3.writeFileSync(store.portFilePath, proxyPort.toString(), { mode: FILE_MODE });
|
|
891
1297
|
writeTlsMarker(store.dir, isTls);
|
|
892
1298
|
writeTldFile(store.dir, tld);
|
|
1299
|
+
writeLanMarker(store.dir, activeLanIp);
|
|
893
1300
|
fixOwnership(store.dir, store.pidPath, store.portFilePath);
|
|
894
1301
|
const proto = isTls ? "HTTPS/2" : "HTTP";
|
|
895
1302
|
const tldLabel = tld !== DEFAULT_TLD ? ` (TLD: .${tld})` : "";
|
|
@@ -897,6 +1304,24 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, strict) {
|
|
|
897
1304
|
console.log(
|
|
898
1305
|
colors_default.green(`${proto} proxy listening on port ${proxyPort}${tldLabel}${modeLabel}`)
|
|
899
1306
|
);
|
|
1307
|
+
if (activeLanIp) {
|
|
1308
|
+
console.log(chalk.green(`LAN mode: ${activeLanIp}`));
|
|
1309
|
+
console.log(chalk.gray("Services are discoverable as <name>.local on your network"));
|
|
1310
|
+
if (isTls) {
|
|
1311
|
+
console.log(chalk.yellow("For HTTPS on devices, install the CA certificate:"));
|
|
1312
|
+
console.log(chalk.gray(` ${path3.join(store.dir, "ca.pem")}`));
|
|
1313
|
+
}
|
|
1314
|
+
if (!lanIpPinned) {
|
|
1315
|
+
lanMonitor = startLanIpMonitor({
|
|
1316
|
+
initialIp: activeLanIp,
|
|
1317
|
+
onChange: (nextIp, previousIp) => updateLanIp(nextIp, previousIp),
|
|
1318
|
+
onError: (error) => {
|
|
1319
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1320
|
+
console.warn(chalk.yellow(`Failed to refresh LAN IP: ${message}`));
|
|
1321
|
+
}
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
900
1325
|
if (redirectServer) {
|
|
901
1326
|
console.log(colors_default.green("HTTP-to-HTTPS redirect listening on port 80"));
|
|
902
1327
|
}
|
|
@@ -907,9 +1332,11 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, strict) {
|
|
|
907
1332
|
exiting = true;
|
|
908
1333
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
909
1334
|
if (pollingInterval) clearInterval(pollingInterval);
|
|
1335
|
+
if (lanMonitor) lanMonitor.stop();
|
|
910
1336
|
if (watcher) {
|
|
911
1337
|
watcher.close();
|
|
912
1338
|
}
|
|
1339
|
+
if (activeLanIp) cleanupAll();
|
|
913
1340
|
if (redirectServer) {
|
|
914
1341
|
redirectServer.close();
|
|
915
1342
|
}
|
|
@@ -1060,8 +1487,11 @@ function listRoutes(store, proxyPort, tls2) {
|
|
|
1060
1487
|
}
|
|
1061
1488
|
console.log();
|
|
1062
1489
|
}
|
|
1063
|
-
async function runApp(initialStore, proxyPort, stateDir, name, commandArgs, tls2, tld, force, autoInfo, desiredPort) {
|
|
1490
|
+
async function runApp(initialStore, proxyPort, stateDir, name, commandArgs, tls2, tld, force, autoInfo, desiredPort, lanMode = false, lanIp) {
|
|
1064
1491
|
let store = initialStore;
|
|
1492
|
+
console.log(chalk.blue.bold(`
|
|
1493
|
+
portless
|
|
1494
|
+
`));
|
|
1065
1495
|
let envTld;
|
|
1066
1496
|
try {
|
|
1067
1497
|
envTld = getDefaultTld();
|
|
@@ -1069,38 +1499,44 @@ async function runApp(initialStore, proxyPort, stateDir, name, commandArgs, tls2
|
|
|
1069
1499
|
console.error(colors_default.red(`Error: ${err.message}`));
|
|
1070
1500
|
process.exit(1);
|
|
1071
1501
|
}
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1502
|
+
const explicit = {
|
|
1503
|
+
useHttps: process.env.PORTLESS_HTTPS !== void 0,
|
|
1504
|
+
customCert: false,
|
|
1505
|
+
lanMode: process.env.PORTLESS_LAN !== void 0,
|
|
1506
|
+
lanIp: process.env.PORTLESS_LAN_IP !== void 0,
|
|
1507
|
+
tld: process.env.PORTLESS_TLD !== void 0,
|
|
1508
|
+
useWildcard: process.env.PORTLESS_WILDCARD !== void 0
|
|
1509
|
+
};
|
|
1510
|
+
const desiredConfig = resolveProxyConfig({
|
|
1511
|
+
persistedLanMode: lanMode,
|
|
1512
|
+
explicit,
|
|
1513
|
+
defaultTld: envTld,
|
|
1514
|
+
useHttps: !isHttpsEnvDisabled(),
|
|
1515
|
+
customCertPath: null,
|
|
1516
|
+
customKeyPath: null,
|
|
1517
|
+
lanMode: isLanEnvEnabled(),
|
|
1518
|
+
lanIp: process.env.PORTLESS_LAN_IP || null,
|
|
1519
|
+
tld: envTld,
|
|
1520
|
+
useWildcard: isWildcardEnvEnabled()
|
|
1521
|
+
});
|
|
1522
|
+
parseHostname(name, tld);
|
|
1523
|
+
const proxyResponsive = await isProxyRunning(proxyPort, tls2);
|
|
1524
|
+
const proxyListeningFromStateDir = !!process.env.PORTLESS_STATE_DIR && await isPortListening(proxyPort);
|
|
1525
|
+
if (!proxyResponsive && !proxyListeningFromStateDir) {
|
|
1526
|
+
const defaultPort = getDefaultPort(desiredConfig.useHttps);
|
|
1093
1527
|
const needsSudo = !isWindows && defaultPort < PRIVILEGED_PORT_THRESHOLD;
|
|
1528
|
+
const manualStartCommand = formatProxyStartCommand(defaultPort, desiredConfig);
|
|
1529
|
+
const fallbackStartCommand = formatProxyStartCommand(FALLBACK_PROXY_PORT, desiredConfig);
|
|
1094
1530
|
if (needsSudo && !process.stdin.isTTY) {
|
|
1095
1531
|
console.error(colors_default.red("Proxy is not running and no TTY is available for sudo."));
|
|
1096
1532
|
console.error(colors_default.blue("Option 1: start the proxy in a terminal (will prompt for sudo):"));
|
|
1097
|
-
console.error(colors_default.cyan(
|
|
1533
|
+
console.error(colors_default.cyan(` ${manualStartCommand}`));
|
|
1098
1534
|
console.error(
|
|
1099
1535
|
colors_default.blue(
|
|
1100
1536
|
`Option 2: use an unprivileged port (no sudo needed, URLs will include :${FALLBACK_PROXY_PORT}):`
|
|
1101
1537
|
)
|
|
1102
1538
|
);
|
|
1103
|
-
console.error(colors_default.cyan(`
|
|
1539
|
+
console.error(colors_default.cyan(` ${fallbackStartCommand}`));
|
|
1104
1540
|
process.exit(1);
|
|
1105
1541
|
}
|
|
1106
1542
|
if (needsSudo && process.stdin.isTTY) {
|
|
@@ -1116,15 +1552,23 @@ portless
|
|
|
1116
1552
|
}
|
|
1117
1553
|
}
|
|
1118
1554
|
console.log(colors_default.yellow("Starting proxy..."));
|
|
1119
|
-
const
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1555
|
+
const proxyStartConfig = buildProxyStartConfig({
|
|
1556
|
+
useHttps: desiredConfig.useHttps,
|
|
1557
|
+
customCertPath: desiredConfig.customCertPath,
|
|
1558
|
+
customKeyPath: desiredConfig.customKeyPath,
|
|
1559
|
+
lanMode: desiredConfig.lanMode,
|
|
1560
|
+
lanIp: desiredConfig.lanIpExplicit ? desiredConfig.lanIp : null,
|
|
1561
|
+
lanIpExplicit: desiredConfig.lanIpExplicit,
|
|
1562
|
+
tld: desiredConfig.tld,
|
|
1563
|
+
useWildcard: desiredConfig.useWildcard
|
|
1564
|
+
});
|
|
1565
|
+
const startArgs = [getEntryScript(), "proxy", "start", ...proxyStartConfig.args];
|
|
1566
|
+
const result = spawnSync2(process.execPath, startArgs, {
|
|
1123
1567
|
stdio: "inherit",
|
|
1124
1568
|
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
1125
1569
|
});
|
|
1126
1570
|
let discovered = null;
|
|
1127
|
-
if (result.
|
|
1571
|
+
if (!result.signal) {
|
|
1128
1572
|
for (let i = 0; i < WAIT_FOR_PROXY_MAX_ATTEMPTS; i++) {
|
|
1129
1573
|
await new Promise((r) => setTimeout(r, WAIT_FOR_PROXY_INTERVAL_MS));
|
|
1130
1574
|
const state = await discoverState();
|
|
@@ -1136,10 +1580,10 @@ portless
|
|
|
1136
1580
|
}
|
|
1137
1581
|
if (!discovered) {
|
|
1138
1582
|
console.error(colors_default.red("Failed to start proxy."));
|
|
1139
|
-
const fallbackDir = resolveStateDir(getDefaultPort(
|
|
1583
|
+
const fallbackDir = resolveStateDir(getDefaultPort(desiredConfig.useHttps));
|
|
1140
1584
|
const logPath = path3.join(fallbackDir, "proxy.log");
|
|
1141
1585
|
console.error(colors_default.blue("Try starting it manually:"));
|
|
1142
|
-
console.error(colors_default.cyan(
|
|
1586
|
+
console.error(colors_default.cyan(` ${manualStartCommand}`));
|
|
1143
1587
|
if (fs3.existsSync(logPath)) {
|
|
1144
1588
|
console.error(colors_default.gray(`Logs: ${logPath}`));
|
|
1145
1589
|
}
|
|
@@ -1150,14 +1594,42 @@ portless
|
|
|
1150
1594
|
stateDir = discovered.dir;
|
|
1151
1595
|
tld = discovered.tld;
|
|
1152
1596
|
tls2 = discovered.tls;
|
|
1597
|
+
lanMode = discovered.lanMode;
|
|
1598
|
+
lanIp = discovered.lanIp;
|
|
1153
1599
|
store = new RouteStore(stateDir, {
|
|
1154
1600
|
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
1155
1601
|
});
|
|
1156
1602
|
console.log(colors_default.green("Proxy started in background"));
|
|
1157
1603
|
} else {
|
|
1158
|
-
|
|
1604
|
+
const runningConfig = readCurrentProxyConfig(stateDir);
|
|
1605
|
+
const mismatchMessages = getProxyConfigMismatchMessages(desiredConfig, runningConfig, explicit);
|
|
1606
|
+
if (mismatchMessages.length > 0) {
|
|
1607
|
+
printProxyConfigMismatch(proxyPort, desiredConfig, mismatchMessages);
|
|
1608
|
+
}
|
|
1609
|
+
lanMode = runningConfig.lanMode;
|
|
1610
|
+
lanIp = runningConfig.lanIp;
|
|
1611
|
+
console.log(chalk.gray("-- Proxy is running"));
|
|
1159
1612
|
}
|
|
1160
1613
|
const hostname = parseHostname(name, tld);
|
|
1614
|
+
if (envTld !== DEFAULT_TLD && envTld !== tld) {
|
|
1615
|
+
console.warn(
|
|
1616
|
+
chalk.yellow(
|
|
1617
|
+
`Warning: PORTLESS_TLD=${envTld} but the running proxy uses .${tld}. Using .${tld}.`
|
|
1618
|
+
)
|
|
1619
|
+
);
|
|
1620
|
+
}
|
|
1621
|
+
if (lanIp) {
|
|
1622
|
+
console.log(chalk.gray(`-- ${hostname} (LAN: ${lanIp})`));
|
|
1623
|
+
} else {
|
|
1624
|
+
console.log(chalk.gray(`-- ${hostname} (auto-resolves to 127.0.0.1)`));
|
|
1625
|
+
}
|
|
1626
|
+
if (autoInfo) {
|
|
1627
|
+
const baseName = autoInfo.prefix ? name.slice(autoInfo.prefix.length + 1) : name;
|
|
1628
|
+
console.log(chalk.gray(`-- Name "${baseName}" (from ${autoInfo.nameSource})`));
|
|
1629
|
+
if (autoInfo.prefix) {
|
|
1630
|
+
console.log(chalk.gray(`-- Prefix "${autoInfo.prefix}" (from ${autoInfo.prefixSource})`));
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1161
1633
|
const port = desiredPort ?? await findFreePort();
|
|
1162
1634
|
if (desiredPort) {
|
|
1163
1635
|
console.log(colors_default.green(`-- Using port ${port} (fixed)`));
|
|
@@ -1178,13 +1650,24 @@ portless
|
|
|
1178
1650
|
console.log(colors_default.yellow(`Killed existing process (PID ${killedPid})`));
|
|
1179
1651
|
}
|
|
1180
1652
|
const finalUrl = formatUrl(hostname, proxyPort, tls2);
|
|
1181
|
-
console.log(
|
|
1653
|
+
console.log(chalk.cyan.bold(`
|
|
1182
1654
|
-> ${finalUrl}
|
|
1183
1655
|
`));
|
|
1656
|
+
if (lanIp) {
|
|
1657
|
+
console.log(chalk.green(` LAN -> ${finalUrl}`));
|
|
1658
|
+
console.log(chalk.gray(" (accessible from other devices on the same WiFi network)\n"));
|
|
1659
|
+
}
|
|
1660
|
+
const basename3 = path3.basename(commandArgs[0]);
|
|
1661
|
+
const isExpo = basename3 === "expo";
|
|
1662
|
+
const isExpoLan = isExpo && (lanMode || isLanEnvEnabled());
|
|
1663
|
+
const hostBind = isExpoLan ? void 0 : "127.0.0.1";
|
|
1664
|
+
if (lanMode && !process.env.PORTLESS_LAN) {
|
|
1665
|
+
process.env.PORTLESS_LAN = "1";
|
|
1666
|
+
}
|
|
1184
1667
|
injectFrameworkFlags(commandArgs, port);
|
|
1185
1668
|
console.log(
|
|
1186
|
-
|
|
1187
|
-
`Running: PORT=${port} HOST
|
|
1669
|
+
chalk.gray(
|
|
1670
|
+
`Running: PORT=${port}${hostBind ? ` HOST=${hostBind}` : ""} PORTLESS_URL=${finalUrl} ${commandArgs.join(" ")}
|
|
1188
1671
|
`
|
|
1189
1672
|
)
|
|
1190
1673
|
);
|
|
@@ -1192,9 +1675,13 @@ portless
|
|
|
1192
1675
|
env: {
|
|
1193
1676
|
...process.env,
|
|
1194
1677
|
PORT: port.toString(),
|
|
1195
|
-
HOST:
|
|
1678
|
+
...hostBind ? { HOST: hostBind } : {},
|
|
1196
1679
|
PORTLESS_URL: finalUrl,
|
|
1197
|
-
__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS: `.${tld}
|
|
1680
|
+
__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS: `.${tld}`,
|
|
1681
|
+
// Note: EXPO_PACKAGER_PROXY_URL is not used — expo-dev-client removed
|
|
1682
|
+
// baked-in pinging, making this env var ineffective. Expo handles its
|
|
1683
|
+
// own LAN discovery natively.
|
|
1684
|
+
...lanMode ? { PORTLESS_LAN: "1" } : {}
|
|
1198
1685
|
},
|
|
1199
1686
|
onCleanup: () => {
|
|
1200
1687
|
try {
|
|
@@ -1342,6 +1829,7 @@ ${colors_default.bold("Install:")}
|
|
|
1342
1829
|
${colors_default.bold("Usage:")}
|
|
1343
1830
|
${colors_default.cyan("portless proxy start")} Start the proxy (HTTPS on port 443, daemon)
|
|
1344
1831
|
${colors_default.cyan("portless proxy start --no-tls")} Start without HTTPS (port 80)
|
|
1832
|
+
${colors_default.cyan("portless proxy start --lan")} Start in LAN mode (mDNS for real device testing)
|
|
1345
1833
|
${colors_default.cyan("portless proxy start -p 1355")} Start on a custom port (no sudo)
|
|
1346
1834
|
${colors_default.cyan("portless proxy stop")} Stop the proxy
|
|
1347
1835
|
${colors_default.cyan("portless <name> <cmd>")} Run your app through the proxy
|
|
@@ -1378,14 +1866,30 @@ ${colors_default.bold("How it works:")}
|
|
|
1378
1866
|
(apps get a random port in the 4000-4999 range via PORT)
|
|
1379
1867
|
3. Access via https://<name>.localhost
|
|
1380
1868
|
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
|
|
1869
|
+
5. Frameworks that ignore PORT (Vite, VitePlus, Astro, React Router, Angular,
|
|
1870
|
+
Expo, React Native) get --port and, when needed, --host flags
|
|
1871
|
+
injected automatically
|
|
1383
1872
|
|
|
1384
1873
|
${colors_default.bold("HTTP/2 + HTTPS (default):")}
|
|
1385
1874
|
HTTPS with HTTP/2 multiplexing is enabled by default (faster page loads).
|
|
1386
1875
|
On first use, portless generates a local CA and adds it to your
|
|
1387
1876
|
system trust store. No browser warnings. Disable with --no-tls.
|
|
1388
1877
|
|
|
1878
|
+
${colors_default.bold("LAN mode:")}
|
|
1879
|
+
Use --lan to make services accessible from other devices (phones,
|
|
1880
|
+
tablets) on the same WiFi network via mDNS (.local domains).
|
|
1881
|
+
Useful for testing React Native / Expo apps on real devices.
|
|
1882
|
+
Expo keeps Metro's default LAN host behavior in this mode.
|
|
1883
|
+
Auto-detected LAN IPs follow network changes automatically.
|
|
1884
|
+
Stopped LAN proxies keep LAN mode for the next start via proxy.lan.
|
|
1885
|
+
Other proxy settings still follow the current flags and env vars.
|
|
1886
|
+
Use PORTLESS_LAN=0 for one start to switch back to .localhost mode.
|
|
1887
|
+
If a proxy is already running with different explicit LAN/TLS/TLD settings,
|
|
1888
|
+
stop it first.
|
|
1889
|
+
${colors_default.cyan("portless proxy start --lan")}
|
|
1890
|
+
${colors_default.cyan("portless proxy start --lan --https")}
|
|
1891
|
+
${colors_default.cyan("portless proxy start --lan --ip 192.168.1.42")}
|
|
1892
|
+
|
|
1389
1893
|
${colors_default.bold("Options:")}
|
|
1390
1894
|
run [--name <name>] <cmd> Infer project name (or override with --name)
|
|
1391
1895
|
Adds worktree prefix in git worktrees
|
|
@@ -1393,6 +1897,8 @@ ${colors_default.bold("Options:")}
|
|
|
1393
1897
|
Standard ports auto-elevate with sudo on macOS/Linux
|
|
1394
1898
|
--no-tls Disable HTTPS (use plain HTTP on port 80)
|
|
1395
1899
|
--https Enable HTTPS (default, accepted for compatibility)
|
|
1900
|
+
--lan Enable LAN mode (mDNS .local, for real device testing)
|
|
1901
|
+
--ip <address> Pin a specific LAN IP (disables auto-follow; use with --lan)
|
|
1396
1902
|
--cert <path> Use a custom TLS certificate
|
|
1397
1903
|
--key <path> Use a custom TLS private key
|
|
1398
1904
|
--foreground Run proxy in foreground (for debugging)
|
|
@@ -1406,7 +1912,8 @@ ${colors_default.bold("Options:")}
|
|
|
1406
1912
|
${colors_default.bold("Environment variables:")}
|
|
1407
1913
|
PORTLESS_PORT=<number> Override the default proxy port (e.g. in .bashrc)
|
|
1408
1914
|
PORTLESS_APP_PORT=<number> Use a fixed port for the app (same as --app-port)
|
|
1409
|
-
PORTLESS_HTTPS
|
|
1915
|
+
PORTLESS_HTTPS=0 Disable HTTPS (same as --no-tls)
|
|
1916
|
+
PORTLESS_LAN=1 Enable LAN mode when set to 1 (set in .bashrc / .zshrc)
|
|
1410
1917
|
PORTLESS_TLD=<tld> Use a custom TLD (e.g. test, dev; default: localhost)
|
|
1411
1918
|
PORTLESS_WILDCARD=1 Allow unregistered subdomains to fall back to parent route
|
|
1412
1919
|
PORTLESS_SYNC_HOSTS=1 Auto-sync ${HOSTS_DISPLAY} (auto-enabled for custom TLDs)
|
|
@@ -1415,8 +1922,9 @@ ${colors_default.bold("Environment variables:")}
|
|
|
1415
1922
|
|
|
1416
1923
|
${colors_default.bold("Child process environment:")}
|
|
1417
1924
|
PORT Ephemeral port the child should listen on
|
|
1418
|
-
HOST
|
|
1925
|
+
HOST Usually 127.0.0.1 (omitted for Expo in LAN mode)
|
|
1419
1926
|
PORTLESS_URL Public URL of the app (e.g. https://myapp.localhost)
|
|
1927
|
+
PORTLESS_LAN Set to 1 when proxy is in LAN mode
|
|
1420
1928
|
|
|
1421
1929
|
${colors_default.bold("Safari / DNS:")}
|
|
1422
1930
|
.localhost subdomains auto-resolve in Chrome, Firefox, and Edge.
|
|
@@ -1438,7 +1946,7 @@ ${colors_default.bold("Reserved names:")}
|
|
|
1438
1946
|
process.exit(0);
|
|
1439
1947
|
}
|
|
1440
1948
|
function printVersion() {
|
|
1441
|
-
console.log("0.
|
|
1949
|
+
console.log("0.10.0");
|
|
1442
1950
|
process.exit(0);
|
|
1443
1951
|
}
|
|
1444
1952
|
async function handleTrust() {
|
|
@@ -1459,7 +1967,7 @@ async function handleTrust() {
|
|
|
1459
1967
|
const isPermissionError = result.error?.includes("Permission denied") || result.error?.includes("EACCES");
|
|
1460
1968
|
if (isPermissionError && !isWindows && process.getuid?.() !== 0) {
|
|
1461
1969
|
console.log(colors_default.yellow("Trusting the CA requires elevated privileges. Requesting sudo..."));
|
|
1462
|
-
const sudoResult =
|
|
1970
|
+
const sudoResult = spawnSync2(
|
|
1463
1971
|
"sudo",
|
|
1464
1972
|
[
|
|
1465
1973
|
"env",
|
|
@@ -1630,7 +2138,7 @@ ${colors_default.bold("Auto-sync:")}
|
|
|
1630
2138
|
`Writing to ${HOSTS_DISPLAY} requires elevated privileges. Requesting sudo...`
|
|
1631
2139
|
)
|
|
1632
2140
|
);
|
|
1633
|
-
const result =
|
|
2141
|
+
const result = spawnSync2(
|
|
1634
2142
|
"sudo",
|
|
1635
2143
|
["env", ...collectPortlessEnvArgs(), process.execPath, getEntryScript(), "hosts", "clean"],
|
|
1636
2144
|
{
|
|
@@ -1683,7 +2191,7 @@ ${colors_default.bold("Usage: portless hosts <command>")}
|
|
|
1683
2191
|
console.log(
|
|
1684
2192
|
colors_default.yellow(`Writing to ${HOSTS_DISPLAY} requires elevated privileges. Requesting sudo...`)
|
|
1685
2193
|
);
|
|
1686
|
-
const result =
|
|
2194
|
+
const result = spawnSync2(
|
|
1687
2195
|
"sudo",
|
|
1688
2196
|
["env", ...collectPortlessEnvArgs(), process.execPath, getEntryScript(), "hosts", "sync"],
|
|
1689
2197
|
{
|
|
@@ -1734,15 +2242,25 @@ ${colors_default.bold("portless proxy")} - Manage the portless proxy server.
|
|
|
1734
2242
|
${colors_default.bold("Usage:")}
|
|
1735
2243
|
${colors_default.cyan("portless proxy start")} Start the HTTPS proxy on port 443 (daemon)
|
|
1736
2244
|
${colors_default.cyan("portless proxy start --no-tls")} Start without HTTPS (port 80)
|
|
2245
|
+
${colors_default.cyan("portless proxy start --lan")} Enable LAN mode (mDNS, .local TLD)
|
|
1737
2246
|
${colors_default.cyan("portless proxy start --foreground")} Start in foreground (for debugging)
|
|
1738
2247
|
${colors_default.cyan("portless proxy start -p 1355")} Start on a custom port (no sudo)
|
|
1739
2248
|
${colors_default.cyan("portless proxy start --tld test")} Use .test instead of .localhost
|
|
1740
2249
|
${colors_default.cyan("portless proxy start --wildcard")} Allow unregistered subdomains to fall back to parent
|
|
1741
2250
|
${colors_default.cyan("portless proxy stop")} Stop the proxy
|
|
2251
|
+
|
|
2252
|
+
${colors_default.bold("LAN mode (--lan):")}
|
|
2253
|
+
Makes services accessible from other devices on the same WiFi network
|
|
2254
|
+
via mDNS (.local domains). Useful for testing on real mobile devices.
|
|
2255
|
+
Auto-detects your LAN IP and follows changes automatically, or use
|
|
2256
|
+
--ip to pin one.
|
|
2257
|
+
Stopped LAN proxies keep LAN mode for the next start via proxy.lan.
|
|
2258
|
+
Use PORTLESS_LAN=0 for one start to switch back to .localhost mode.
|
|
1742
2259
|
`);
|
|
1743
2260
|
process.exit(isProxyHelp || !args[1] ? 0 : 1);
|
|
1744
2261
|
}
|
|
1745
2262
|
const isForeground = args.includes("--foreground");
|
|
2263
|
+
const hasHttpsFlag = args.includes("--https");
|
|
1746
2264
|
const hasNoTls = args.includes("--no-tls") || isHttpsEnvDisabled();
|
|
1747
2265
|
const wantHttps = !hasNoTls;
|
|
1748
2266
|
let customCertPath = null;
|
|
@@ -1767,7 +2285,7 @@ ${colors_default.bold("Usage:")}
|
|
|
1767
2285
|
console.error(colors_default.red("Error: --cert and --key must be used together."));
|
|
1768
2286
|
process.exit(1);
|
|
1769
2287
|
}
|
|
1770
|
-
|
|
2288
|
+
let useHttps = wantHttps || !!(customCertPath && customKeyPath);
|
|
1771
2289
|
let hasExplicitPort = false;
|
|
1772
2290
|
let proxyPort = getDefaultPort(useHttps);
|
|
1773
2291
|
let portFlagIndex = args.indexOf("--port");
|
|
@@ -1809,12 +2327,66 @@ ${colors_default.bold("Usage:")}
|
|
|
1809
2327
|
process.exit(1);
|
|
1810
2328
|
}
|
|
1811
2329
|
}
|
|
2330
|
+
const useWildcard = args.includes("--wildcard") || isWildcardEnvEnabled();
|
|
2331
|
+
const explicit = {
|
|
2332
|
+
useHttps: hasHttpsFlag || hasNoTls || customCertPath !== null || customKeyPath !== null || process.env.PORTLESS_HTTPS !== void 0,
|
|
2333
|
+
customCert: customCertPath !== null || customKeyPath !== null,
|
|
2334
|
+
lanMode: process.env.PORTLESS_LAN !== void 0,
|
|
2335
|
+
lanIp: process.env.PORTLESS_LAN_IP !== void 0,
|
|
2336
|
+
tld: tldIdx !== -1 || process.env.PORTLESS_TLD !== void 0,
|
|
2337
|
+
useWildcard: args.includes("--wildcard") || process.env.PORTLESS_WILDCARD !== void 0
|
|
2338
|
+
};
|
|
2339
|
+
let stateDir = resolveStateDir(proxyPort);
|
|
2340
|
+
let persistedLanMode = readLanMarker(stateDir) !== null;
|
|
2341
|
+
let runningPort = null;
|
|
2342
|
+
if (!hasExplicitPort) {
|
|
2343
|
+
const currentState = await discoverState();
|
|
2344
|
+
persistedLanMode = currentState.lanMode;
|
|
2345
|
+
if (await isProxyRunning(currentState.port) || !!process.env.PORTLESS_STATE_DIR && await isPortListening(currentState.port)) {
|
|
2346
|
+
runningPort = currentState.port;
|
|
2347
|
+
proxyPort = currentState.port;
|
|
2348
|
+
stateDir = currentState.dir;
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
const desiredConfig = resolveProxyConfig({
|
|
2352
|
+
persistedLanMode,
|
|
2353
|
+
explicit,
|
|
2354
|
+
defaultTld: getDefaultTld(),
|
|
2355
|
+
useHttps: wantHttps || !!(customCertPath && customKeyPath),
|
|
2356
|
+
customCertPath,
|
|
2357
|
+
customKeyPath,
|
|
2358
|
+
lanMode: isLanEnvEnabled(),
|
|
2359
|
+
lanIp: process.env.PORTLESS_LAN_IP || null,
|
|
2360
|
+
tld,
|
|
2361
|
+
useWildcard
|
|
2362
|
+
});
|
|
2363
|
+
const lanMode = desiredConfig.lanMode;
|
|
2364
|
+
useHttps = desiredConfig.useHttps;
|
|
2365
|
+
customCertPath = desiredConfig.customCertPath;
|
|
2366
|
+
customKeyPath = desiredConfig.customKeyPath;
|
|
2367
|
+
tld = desiredConfig.tld;
|
|
2368
|
+
const desiredWildcard = desiredConfig.useWildcard;
|
|
2369
|
+
let lanIp = desiredConfig.lanIpExplicit ? desiredConfig.lanIp : null;
|
|
2370
|
+
if (!hasExplicitPort && runningPort === null) {
|
|
2371
|
+
proxyPort = getDefaultPort(useHttps);
|
|
2372
|
+
stateDir = resolveStateDir(proxyPort);
|
|
2373
|
+
}
|
|
2374
|
+
if (lanMode && tldIdx !== -1) {
|
|
2375
|
+
const userTld = args[tldIdx + 1];
|
|
2376
|
+
if (userTld && userTld !== "local") {
|
|
2377
|
+
console.warn(
|
|
2378
|
+
chalk.yellow(
|
|
2379
|
+
`Warning: --lan forces .local TLD (mDNS requirement). Ignoring --tld ${userTld}.`
|
|
2380
|
+
)
|
|
2381
|
+
);
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
1812
2384
|
const riskyReason = RISKY_TLDS.get(tld);
|
|
1813
|
-
if (riskyReason) {
|
|
2385
|
+
if (riskyReason && !lanMode) {
|
|
1814
2386
|
console.warn(colors_default.yellow(`Warning: .${tld}: ${riskyReason}`));
|
|
1815
2387
|
}
|
|
1816
2388
|
const syncDisabled = process.env.PORTLESS_SYNC_HOSTS === "0" || process.env.PORTLESS_SYNC_HOSTS === "false";
|
|
1817
|
-
if (tld !== DEFAULT_TLD && syncDisabled) {
|
|
2389
|
+
if (tld !== DEFAULT_TLD && !lanMode && syncDisabled) {
|
|
1818
2390
|
console.warn(
|
|
1819
2391
|
colors_default.yellow(
|
|
1820
2392
|
`Warning: .${tld} domains require ${HOSTS_DISPLAY} entries to resolve to 127.0.0.1.`
|
|
@@ -1823,51 +2395,93 @@ ${colors_default.bold("Usage:")}
|
|
|
1823
2395
|
console.warn(colors_default.yellow("Hosts sync is disabled. To add entries manually, run:"));
|
|
1824
2396
|
console.warn(colors_default.cyan(" portless hosts sync"));
|
|
1825
2397
|
}
|
|
1826
|
-
const useWildcard = args.includes("--wildcard") || isWildcardEnvEnabled();
|
|
1827
|
-
let stateDir = resolveStateDir(proxyPort);
|
|
1828
2398
|
let store = new RouteStore(stateDir, {
|
|
1829
2399
|
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
1830
2400
|
});
|
|
1831
|
-
|
|
2401
|
+
const proxyRunning = runningPort !== null || await isProxyRunning(proxyPort);
|
|
2402
|
+
if (proxyRunning) {
|
|
2403
|
+
const runningConfig = readCurrentProxyConfig(stateDir);
|
|
2404
|
+
const mismatchMessages = getProxyConfigMismatchMessages(desiredConfig, runningConfig, explicit);
|
|
2405
|
+
if (mismatchMessages.length > 0) {
|
|
2406
|
+
printProxyConfigMismatch(proxyPort, desiredConfig, mismatchMessages);
|
|
2407
|
+
}
|
|
1832
2408
|
if (isForeground) {
|
|
1833
2409
|
return;
|
|
1834
2410
|
}
|
|
1835
|
-
const portFlag = proxyPort !==
|
|
2411
|
+
const portFlag = proxyPort !== getDefaultPort(useHttps) ? ` -p ${proxyPort}` : "";
|
|
1836
2412
|
console.log(colors_default.yellow(`Proxy is already running on port ${proxyPort}.`));
|
|
1837
2413
|
console.log(
|
|
1838
2414
|
colors_default.blue(`To restart: portless proxy stop${portFlag} && portless proxy start${portFlag}`)
|
|
1839
2415
|
);
|
|
1840
2416
|
return;
|
|
1841
2417
|
}
|
|
2418
|
+
if (lanMode) {
|
|
2419
|
+
const mdnsSupport = isMdnsSupported();
|
|
2420
|
+
if (!mdnsSupport.supported) {
|
|
2421
|
+
console.error(
|
|
2422
|
+
colors_default.red(
|
|
2423
|
+
"Error: LAN mode requires mDNS publishing, which is not supported on this platform."
|
|
2424
|
+
)
|
|
2425
|
+
);
|
|
2426
|
+
if (mdnsSupport.reason) {
|
|
2427
|
+
console.error(colors_default.gray(mdnsSupport.reason));
|
|
2428
|
+
}
|
|
2429
|
+
process.exit(1);
|
|
2430
|
+
}
|
|
2431
|
+
const inheritedLanIp = process.env[INTERNAL_LAN_IP_ENV] || null;
|
|
2432
|
+
delete process.env[INTERNAL_LAN_IP_ENV];
|
|
2433
|
+
if (!lanIp) {
|
|
2434
|
+
lanIp = inheritedLanIp || await getLocalNetworkIp();
|
|
2435
|
+
}
|
|
2436
|
+
if (!lanIp) {
|
|
2437
|
+
console.error(colors_default.red("Error: Could not detect LAN IP. Are you connected to a network?"));
|
|
2438
|
+
console.error(colors_default.blue("Specify manually:"));
|
|
2439
|
+
console.error(colors_default.cyan(" portless proxy start --lan --ip 192.168.1.42"));
|
|
2440
|
+
process.exit(1);
|
|
2441
|
+
}
|
|
2442
|
+
} else {
|
|
2443
|
+
delete process.env[INTERNAL_LAN_IP_ENV];
|
|
2444
|
+
}
|
|
2445
|
+
const resolvedConfig = {
|
|
2446
|
+
...desiredConfig,
|
|
2447
|
+
useHttps,
|
|
2448
|
+
customCertPath,
|
|
2449
|
+
customKeyPath,
|
|
2450
|
+
lanMode,
|
|
2451
|
+
lanIp: desiredConfig.lanIpExplicit ? lanIp : null,
|
|
2452
|
+
lanIpExplicit: desiredConfig.lanIpExplicit,
|
|
2453
|
+
tld,
|
|
2454
|
+
useWildcard: desiredWildcard
|
|
2455
|
+
};
|
|
1842
2456
|
if (!isWindows && proxyPort < PRIVILEGED_PORT_THRESHOLD && (process.getuid?.() ?? -1) !== 0) {
|
|
1843
|
-
const
|
|
2457
|
+
const startArgs = [
|
|
1844
2458
|
process.execPath,
|
|
1845
2459
|
getEntryScript(),
|
|
1846
2460
|
"proxy",
|
|
1847
2461
|
"start",
|
|
1848
|
-
|
|
1849
|
-
|
|
2462
|
+
...buildProxyStartConfig({
|
|
2463
|
+
useHttps,
|
|
2464
|
+
customCertPath,
|
|
2465
|
+
customKeyPath,
|
|
2466
|
+
lanMode,
|
|
2467
|
+
lanIp: desiredConfig.lanIpExplicit ? lanIp : null,
|
|
2468
|
+
lanIpExplicit: desiredConfig.lanIpExplicit,
|
|
2469
|
+
tld,
|
|
2470
|
+
useWildcard: desiredWildcard,
|
|
2471
|
+
foreground: isForeground,
|
|
2472
|
+
includePort: true,
|
|
2473
|
+
proxyPort
|
|
2474
|
+
}).args
|
|
1850
2475
|
];
|
|
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("");
|
|
2476
|
+
const fallbackCommand = formatProxyStartCommand(FALLBACK_PROXY_PORT, resolvedConfig);
|
|
2477
|
+
const currentCommand = formatProxyStartCommand(proxyPort, resolvedConfig);
|
|
1860
2478
|
console.log(
|
|
1861
2479
|
colors_default.yellow(`Port ${proxyPort} requires elevated privileges. Requesting sudo...`)
|
|
1862
2480
|
);
|
|
1863
2481
|
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
|
-
);
|
|
2482
|
+
console.log(colors_default.gray(`(To skip sudo, use an unprivileged port: ${fallbackCommand})`));
|
|
1869
2483
|
}
|
|
1870
|
-
const result =
|
|
2484
|
+
const result = spawnSync2("sudo", ["env", ...collectPortlessEnvArgs(), ...startArgs], {
|
|
1871
2485
|
stdio: "inherit",
|
|
1872
2486
|
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
1873
2487
|
});
|
|
@@ -1894,7 +2508,7 @@ ${colors_default.bold("Usage:")}
|
|
|
1894
2508
|
console.log(
|
|
1895
2509
|
colors_default.blue(`For clean URLs without port numbers, re-run and accept the sudo prompt:`)
|
|
1896
2510
|
);
|
|
1897
|
-
console.log(colors_default.cyan(`
|
|
2511
|
+
console.log(colors_default.cyan(` ${fallbackCommand}`));
|
|
1898
2512
|
if (await isProxyRunning(proxyPort)) {
|
|
1899
2513
|
console.log(colors_default.yellow(`Proxy is already running on port ${proxyPort}.`));
|
|
1900
2514
|
return;
|
|
@@ -1908,7 +2522,7 @@ ${colors_default.bold("Usage:")}
|
|
|
1908
2522
|
colors_default.red(`Error: Port ${proxyPort} requires elevated privileges and sudo failed.`)
|
|
1909
2523
|
);
|
|
1910
2524
|
console.error(colors_default.blue("Try again (portless will prompt for sudo):"));
|
|
1911
|
-
console.error(colors_default.cyan(`
|
|
2525
|
+
console.error(colors_default.cyan(` ${currentCommand}`));
|
|
1912
2526
|
process.exit(1);
|
|
1913
2527
|
}
|
|
1914
2528
|
}
|
|
@@ -1975,8 +2589,8 @@ ${colors_default.bold("Usage:")}
|
|
|
1975
2589
|
}
|
|
1976
2590
|
}
|
|
1977
2591
|
if (isForeground) {
|
|
1978
|
-
console.log(
|
|
1979
|
-
startProxyServer(store, proxyPort, tld, tlsOptions,
|
|
2592
|
+
console.log(chalk.blue.bold("\nportless proxy\n"));
|
|
2593
|
+
startProxyServer(store, proxyPort, tld, tlsOptions, lanIp, desiredWildcard ? false : void 0);
|
|
1980
2594
|
return;
|
|
1981
2595
|
}
|
|
1982
2596
|
store.ensureDir();
|
|
@@ -1992,26 +2606,21 @@ ${colors_default.bold("Usage:")}
|
|
|
1992
2606
|
getEntryScript(),
|
|
1993
2607
|
"proxy",
|
|
1994
2608
|
"start",
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
2609
|
+
...buildProxyStartConfig({
|
|
2610
|
+
useHttps,
|
|
2611
|
+
customCertPath,
|
|
2612
|
+
customKeyPath,
|
|
2613
|
+
lanMode,
|
|
2614
|
+
lanIp: desiredConfig.lanIpExplicit ? lanIp : null,
|
|
2615
|
+
lanIpExplicit: desiredConfig.lanIpExplicit,
|
|
2616
|
+
tld,
|
|
2617
|
+
useWildcard: desiredWildcard,
|
|
2618
|
+
foreground: true,
|
|
2619
|
+
includePort: true,
|
|
2620
|
+
proxyPort
|
|
2621
|
+
}).args
|
|
1998
2622
|
];
|
|
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, {
|
|
2623
|
+
const child = spawn2(process.execPath, daemonArgs, {
|
|
2015
2624
|
detached: true,
|
|
2016
2625
|
stdio: ["ignore", logFd, logFd],
|
|
2017
2626
|
env: process.env,
|
|
@@ -2031,7 +2640,11 @@ ${colors_default.bold("Usage:")}
|
|
|
2031
2640
|
process.exit(1);
|
|
2032
2641
|
}
|
|
2033
2642
|
const proto = useHttps ? "HTTPS/2" : "HTTP";
|
|
2034
|
-
console.log(
|
|
2643
|
+
console.log(chalk.green(`${proto} proxy started on port ${proxyPort}`));
|
|
2644
|
+
if (lanMode && lanIp) {
|
|
2645
|
+
console.log(chalk.green(`LAN mode active. IP: ${lanIp}`));
|
|
2646
|
+
console.log(chalk.gray("Services will be discoverable as <name>.local on your network."));
|
|
2647
|
+
}
|
|
2035
2648
|
}
|
|
2036
2649
|
async function handleRunMode(args) {
|
|
2037
2650
|
const parsed = parseRunArgs(args);
|
|
@@ -2055,7 +2668,7 @@ async function handleRunMode(args) {
|
|
|
2055
2668
|
}
|
|
2056
2669
|
const worktree = detectWorktreePrefix();
|
|
2057
2670
|
const effectiveName = worktree ? `${worktree.prefix}.${baseName}` : baseName;
|
|
2058
|
-
const { dir, port, tls: tls2, tld } = await discoverState();
|
|
2671
|
+
const { dir, port, tls: tls2, tld, lanMode, lanIp } = await discoverState();
|
|
2059
2672
|
const store = new RouteStore(dir, {
|
|
2060
2673
|
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
2061
2674
|
});
|
|
@@ -2069,7 +2682,9 @@ async function handleRunMode(args) {
|
|
|
2069
2682
|
tld,
|
|
2070
2683
|
parsed.force,
|
|
2071
2684
|
{ nameSource, prefix: worktree?.prefix, prefixSource: worktree?.source },
|
|
2072
|
-
parsed.appPort
|
|
2685
|
+
parsed.appPort,
|
|
2686
|
+
lanMode,
|
|
2687
|
+
lanIp
|
|
2073
2688
|
);
|
|
2074
2689
|
}
|
|
2075
2690
|
async function handleNamedMode(args) {
|
|
@@ -2083,7 +2698,7 @@ async function handleNamedMode(args) {
|
|
|
2083
2698
|
process.exit(1);
|
|
2084
2699
|
}
|
|
2085
2700
|
const safeName = parsed.name.split(".").map((label) => truncateLabel(label)).join(".");
|
|
2086
|
-
const { dir, port, tls: tls2, tld } = await discoverState();
|
|
2701
|
+
const { dir, port, tls: tls2, tld, lanMode, lanIp } = await discoverState();
|
|
2087
2702
|
const store = new RouteStore(dir, {
|
|
2088
2703
|
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
2089
2704
|
});
|
|
@@ -2097,7 +2712,9 @@ async function handleNamedMode(args) {
|
|
|
2097
2712
|
tld,
|
|
2098
2713
|
parsed.force,
|
|
2099
2714
|
void 0,
|
|
2100
|
-
parsed.appPort
|
|
2715
|
+
parsed.appPort,
|
|
2716
|
+
lanMode,
|
|
2717
|
+
lanIp
|
|
2101
2718
|
);
|
|
2102
2719
|
}
|
|
2103
2720
|
async function main() {
|
|
@@ -2119,6 +2736,40 @@ async function main() {
|
|
|
2119
2736
|
console.error(colors_default.cyan(" npm install -D portless"));
|
|
2120
2737
|
process.exit(1);
|
|
2121
2738
|
}
|
|
2739
|
+
const stripGlobalFlag = (flag, hasValue) => {
|
|
2740
|
+
const sep = args.indexOf("--");
|
|
2741
|
+
const end = sep === -1 ? args.length : sep;
|
|
2742
|
+
const idx = args.indexOf(flag);
|
|
2743
|
+
if (idx === -1 || idx >= end) return null;
|
|
2744
|
+
if (!hasValue) {
|
|
2745
|
+
args.splice(idx, 1);
|
|
2746
|
+
return true;
|
|
2747
|
+
}
|
|
2748
|
+
const value = args[idx + 1];
|
|
2749
|
+
if (!value || value.startsWith("-")) return false;
|
|
2750
|
+
args.splice(idx, 2);
|
|
2751
|
+
return value;
|
|
2752
|
+
};
|
|
2753
|
+
if (stripGlobalFlag("--lan", false)) {
|
|
2754
|
+
process.env.PORTLESS_LAN = "1";
|
|
2755
|
+
}
|
|
2756
|
+
const ipResult = stripGlobalFlag("--ip", true);
|
|
2757
|
+
if (ipResult === false) {
|
|
2758
|
+
console.error(chalk.red("Error: --ip requires an IP address."));
|
|
2759
|
+
console.error(chalk.cyan(" portless --lan --ip 192.168.1.42 run <command>"));
|
|
2760
|
+
process.exit(1);
|
|
2761
|
+
} else if (typeof ipResult === "string") {
|
|
2762
|
+
process.env.PORTLESS_LAN_IP = ipResult;
|
|
2763
|
+
process.env.PORTLESS_LAN = "1";
|
|
2764
|
+
}
|
|
2765
|
+
const autoIpResult = stripGlobalFlag(INTERNAL_LAN_IP_FLAG, true);
|
|
2766
|
+
if (autoIpResult === false) {
|
|
2767
|
+
console.error(chalk.red(`Error: ${INTERNAL_LAN_IP_FLAG} requires an IP address.`));
|
|
2768
|
+
process.exit(1);
|
|
2769
|
+
} else if (typeof autoIpResult === "string") {
|
|
2770
|
+
process.env[INTERNAL_LAN_IP_ENV] = autoIpResult;
|
|
2771
|
+
process.env.PORTLESS_LAN = "1";
|
|
2772
|
+
}
|
|
2122
2773
|
if (args[0] === "--name") {
|
|
2123
2774
|
args.shift();
|
|
2124
2775
|
if (!args[0]) {
|