portless 0.15.3 → 0.15.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/cli.js +82 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -243,6 +243,8 @@ flowchart TD
|
|
|
243
243
|
2. **Run apps**: `portless <name> <command>` assigns a free port and registers with the proxy
|
|
244
244
|
3. **Access via URL**: `https://<name>.localhost` routes through the proxy to your app
|
|
245
245
|
|
|
246
|
+
Outside LAN mode, the proxy and its HTTP redirect listener bind only to the IPv4 and IPv6 loopback addresses, `127.0.0.1` and `::1`. They do not accept connections through LAN, VPN, or other network interfaces.
|
|
247
|
+
|
|
246
248
|
## HTTP/2 + HTTPS
|
|
247
249
|
|
|
248
250
|
HTTPS with HTTP/2 is enabled by default. Browsers limit HTTP/1.1 to 6 connections per host, which bottlenecks dev servers that serve many unbundled files (Vite, Nuxt, etc.). HTTP/2 multiplexes all requests over a single connection.
|
|
@@ -287,7 +289,7 @@ portless proxy start --lan --https
|
|
|
287
289
|
portless proxy start --lan --ip 192.168.1.42
|
|
288
290
|
```
|
|
289
291
|
|
|
290
|
-
`--lan`
|
|
292
|
+
`--lan` explicitly binds the proxy to the IPv4 and IPv6 unspecified addresses, `0.0.0.0` and `::`, and switches to mDNS discovery. This makes services available as `<name>.local` to devices on the same network. Portless auto-detects your LAN IP and follows Wi-Fi/IP changes automatically, but you can pin another address with `--ip <address>` or by exporting `PORTLESS_LAN_IP`. Set `PORTLESS_LAN=1` in your shell (0/1 boolean) to make LAN mode the default whenever the proxy starts.
|
|
291
293
|
|
|
292
294
|
Portless remembers LAN mode via `proxy.lan`, so if you stop a LAN proxy and start it again, it stays in LAN mode. All proxy settings (port, TLS, TLDs, LAN) are persisted and reused on auto-start unless overridden by explicit flags or env vars. Use `PORTLESS_LAN=0` for one start to switch back to `.localhost` mode. If a proxy is already running with different explicit LAN/TLS/TLD settings, portless warns and asks you to stop it first.
|
|
293
295
|
|
package/dist/cli.js
CHANGED
|
@@ -1450,6 +1450,10 @@ var FALLBACK_PROXY_PORT = 1355;
|
|
|
1450
1450
|
var PRIVILEGED_PORT_THRESHOLD = 1024;
|
|
1451
1451
|
var INTERNAL_LAN_IP_ENV = "PORTLESS_INTERNAL_LAN_IP";
|
|
1452
1452
|
var INTERNAL_LAN_IP_FLAG = "--lan-ip-auto";
|
|
1453
|
+
var IPV4_LOOPBACK_PROXY_HOST = "127.0.0.1";
|
|
1454
|
+
var IPV6_LOOPBACK_PROXY_HOST = "::1";
|
|
1455
|
+
var IPV4_LAN_PROXY_HOST = "0.0.0.0";
|
|
1456
|
+
var IPV6_LAN_PROXY_HOST = "::";
|
|
1453
1457
|
var LEGACY_SYSTEM_STATE_DIR = isWindows ? path3.join(os2.tmpdir(), "portless") : "/tmp/portless";
|
|
1454
1458
|
var USER_STATE_DIR = path3.join(resolveUserHome(), ".portless");
|
|
1455
1459
|
var MIN_APP_PORT = 4e3;
|
|
@@ -1552,6 +1556,12 @@ var SIGNAL_CODES = {
|
|
|
1552
1556
|
SIGKILL: 9,
|
|
1553
1557
|
SIGTERM: 15
|
|
1554
1558
|
};
|
|
1559
|
+
function getProxyBindTargets(lanMode) {
|
|
1560
|
+
return lanMode ? [{ host: IPV4_LAN_PROXY_HOST }, { host: IPV6_LAN_PROXY_HOST, ipv6Only: true }] : [{ host: IPV4_LOOPBACK_PROXY_HOST }, { host: IPV6_LOOPBACK_PROXY_HOST, ipv6Only: true }];
|
|
1561
|
+
}
|
|
1562
|
+
function listenOnProxyInterface(server, port, target, listener) {
|
|
1563
|
+
server.listen({ port, host: target.host, ipv6Only: target.ipv6Only }, listener);
|
|
1564
|
+
}
|
|
1555
1565
|
function killTree(child, signal = "SIGTERM") {
|
|
1556
1566
|
if (!child.pid) {
|
|
1557
1567
|
child.kill(signal);
|
|
@@ -4102,6 +4112,12 @@ function formatUrls(hostnames, proxyPort, tls2) {
|
|
|
4102
4112
|
function formatViteAllowedHosts(tlds) {
|
|
4103
4113
|
return tlds.map((configuredTld) => `.${configuredTld}`).join(",");
|
|
4104
4114
|
}
|
|
4115
|
+
function formatBindEndpoint(host, port) {
|
|
4116
|
+
return host.includes(":") ? `[${host}]:${port}` : `${host}:${port}`;
|
|
4117
|
+
}
|
|
4118
|
+
function isUnavailableIpv6Bind(err, host) {
|
|
4119
|
+
return host.includes(":") && (err.code === "EAFNOSUPPORT" || err.code === "EADDRNOTAVAIL");
|
|
4120
|
+
}
|
|
4105
4121
|
function addRoutes(store, hostnames, port, pid, force = false) {
|
|
4106
4122
|
const registered = [];
|
|
4107
4123
|
const killedPids = [];
|
|
@@ -4137,6 +4153,9 @@ function startProxyServer(store, proxyPort, tld, tlds, tlsOptions, lanIp, strict
|
|
|
4137
4153
|
const isTls = !!tlsOptions;
|
|
4138
4154
|
const mdnsSupport = isMdnsSupported();
|
|
4139
4155
|
let activeLanIp = lanIp && mdnsSupport.supported ? lanIp : null;
|
|
4156
|
+
const lanModeActive = activeLanIp !== null;
|
|
4157
|
+
const bindTargets = getProxyBindTargets(lanModeActive);
|
|
4158
|
+
const primaryBindTarget = bindTargets[0];
|
|
4140
4159
|
const lanIpPinned = !!process.env.PORTLESS_LAN_IP;
|
|
4141
4160
|
let lanMonitor = null;
|
|
4142
4161
|
if (lanIp && !mdnsSupport.supported) {
|
|
@@ -4220,7 +4239,7 @@ function startProxyServer(store, proxyPort, tld, tlds, tlsOptions, lanIp, strict
|
|
|
4220
4239
|
syncHostsFile(cachedRoutes.map((r) => r.hostname));
|
|
4221
4240
|
}
|
|
4222
4241
|
publishCachedRoutes();
|
|
4223
|
-
const
|
|
4242
|
+
const createServer2 = () => createProxyServer({
|
|
4224
4243
|
getRoutes: () => cachedRoutes,
|
|
4225
4244
|
proxyPort,
|
|
4226
4245
|
tld,
|
|
@@ -4229,6 +4248,17 @@ function startProxyServer(store, proxyPort, tld, tlds, tlsOptions, lanIp, strict
|
|
|
4229
4248
|
onError: (msg) => console.error(colors_default.red(msg)),
|
|
4230
4249
|
tls: tlsOptions
|
|
4231
4250
|
});
|
|
4251
|
+
const server = createServer2();
|
|
4252
|
+
const additionalServers = /* @__PURE__ */ new Set();
|
|
4253
|
+
const redirectServers = /* @__PURE__ */ new Set();
|
|
4254
|
+
const closeAuxiliaryServers = () => {
|
|
4255
|
+
for (const auxiliaryServer of [...additionalServers, ...redirectServers]) {
|
|
4256
|
+
try {
|
|
4257
|
+
auxiliaryServer.close();
|
|
4258
|
+
} catch {
|
|
4259
|
+
}
|
|
4260
|
+
}
|
|
4261
|
+
};
|
|
4232
4262
|
server.on("error", (err) => {
|
|
4233
4263
|
if (err.code === "EADDRINUSE") {
|
|
4234
4264
|
console.error(colors_default.red(`Port ${proxyPort} is already in use.`));
|
|
@@ -4247,18 +4277,50 @@ function startProxyServer(store, proxyPort, tld, tlds, tlsOptions, lanIp, strict
|
|
|
4247
4277
|
} else {
|
|
4248
4278
|
console.error(colors_default.red(`Proxy error: ${err.message}`));
|
|
4249
4279
|
}
|
|
4250
|
-
|
|
4280
|
+
closeAuxiliaryServers();
|
|
4251
4281
|
process.exit(1);
|
|
4252
4282
|
});
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4283
|
+
const proto = isTls ? "HTTPS/2" : "HTTP";
|
|
4284
|
+
const tldLabel = tlds.length > 1 || tld !== DEFAULT_TLD ? ` (TLDs: ${formatTldList2(tlds)})` : "";
|
|
4285
|
+
const modeLabel = strict === false ? " (wildcard)" : "";
|
|
4286
|
+
for (const bindTarget of bindTargets.slice(1)) {
|
|
4287
|
+
const additionalServer = createServer2();
|
|
4288
|
+
additionalServers.add(additionalServer);
|
|
4289
|
+
additionalServer.on("error", (err) => {
|
|
4290
|
+
additionalServers.delete(additionalServer);
|
|
4291
|
+
if (!isUnavailableIpv6Bind(err, bindTarget.host)) {
|
|
4292
|
+
console.warn(
|
|
4293
|
+
colors_default.yellow(
|
|
4294
|
+
`Could not listen on ${formatBindEndpoint(bindTarget.host, proxyPort)}: ${err.message}`
|
|
4295
|
+
)
|
|
4296
|
+
);
|
|
4297
|
+
}
|
|
4298
|
+
});
|
|
4299
|
+
listenOnProxyInterface(additionalServer, proxyPort, bindTarget, () => {
|
|
4300
|
+
console.log(
|
|
4301
|
+
colors_default.green(
|
|
4302
|
+
`${proto} proxy listening on ${formatBindEndpoint(bindTarget.host, proxyPort)}${tldLabel}${modeLabel}`
|
|
4303
|
+
)
|
|
4304
|
+
);
|
|
4258
4305
|
});
|
|
4259
|
-
redirectServer.listen(80);
|
|
4260
4306
|
}
|
|
4261
|
-
|
|
4307
|
+
if (isTls && proxyPort !== 80) {
|
|
4308
|
+
for (const bindTarget of bindTargets) {
|
|
4309
|
+
const redirectServer = createHttpRedirectServer(proxyPort);
|
|
4310
|
+
redirectServers.add(redirectServer);
|
|
4311
|
+
redirectServer.on("error", () => {
|
|
4312
|
+
redirectServers.delete(redirectServer);
|
|
4313
|
+
});
|
|
4314
|
+
listenOnProxyInterface(redirectServer, 80, bindTarget, () => {
|
|
4315
|
+
console.log(
|
|
4316
|
+
colors_default.green(
|
|
4317
|
+
`HTTP-to-HTTPS redirect listening on ${formatBindEndpoint(bindTarget.host, 80)}`
|
|
4318
|
+
)
|
|
4319
|
+
);
|
|
4320
|
+
});
|
|
4321
|
+
}
|
|
4322
|
+
}
|
|
4323
|
+
listenOnProxyInterface(server, proxyPort, primaryBindTarget, () => {
|
|
4262
4324
|
fs10.writeFileSync(store.pidPath, process.pid.toString(), { mode: FILE_MODE });
|
|
4263
4325
|
fs10.writeFileSync(store.portFilePath, proxyPort.toString(), { mode: FILE_MODE });
|
|
4264
4326
|
writeTlsMarker(store.dir, isTls);
|
|
@@ -4266,11 +4328,10 @@ function startProxyServer(store, proxyPort, tld, tlds, tlsOptions, lanIp, strict
|
|
|
4266
4328
|
writeTldsFile(store.dir, tlds);
|
|
4267
4329
|
writeLanMarker(store.dir, activeLanIp);
|
|
4268
4330
|
fixOwnership(store.dir, store.pidPath, store.portFilePath);
|
|
4269
|
-
const proto = isTls ? "HTTPS/2" : "HTTP";
|
|
4270
|
-
const tldLabel = tlds.length > 1 || tld !== DEFAULT_TLD ? ` (TLDs: ${formatTldList2(tlds)})` : "";
|
|
4271
|
-
const modeLabel = strict === false ? " (wildcard)" : "";
|
|
4272
4331
|
console.log(
|
|
4273
|
-
colors_default.green(
|
|
4332
|
+
colors_default.green(
|
|
4333
|
+
`${proto} proxy listening on ${formatBindEndpoint(primaryBindTarget.host, proxyPort)}${tldLabel}${modeLabel}`
|
|
4334
|
+
)
|
|
4274
4335
|
);
|
|
4275
4336
|
if (activeLanIp) {
|
|
4276
4337
|
console.log(chalk.green(`LAN mode: ${activeLanIp}`));
|
|
@@ -4290,9 +4351,6 @@ function startProxyServer(store, proxyPort, tld, tlds, tlsOptions, lanIp, strict
|
|
|
4290
4351
|
});
|
|
4291
4352
|
}
|
|
4292
4353
|
}
|
|
4293
|
-
if (redirectServer) {
|
|
4294
|
-
console.log(colors_default.green("HTTP-to-HTTPS redirect listening on port 80"));
|
|
4295
|
-
}
|
|
4296
4354
|
});
|
|
4297
4355
|
let exiting = false;
|
|
4298
4356
|
const cleanup = () => {
|
|
@@ -4305,9 +4363,7 @@ function startProxyServer(store, proxyPort, tld, tlds, tlsOptions, lanIp, strict
|
|
|
4305
4363
|
watcher.close();
|
|
4306
4364
|
}
|
|
4307
4365
|
if (activeLanIp) cleanupAll();
|
|
4308
|
-
|
|
4309
|
-
redirectServer.close();
|
|
4310
|
-
}
|
|
4366
|
+
closeAuxiliaryServers();
|
|
4311
4367
|
try {
|
|
4312
4368
|
fs10.unlinkSync(store.pidPath);
|
|
4313
4369
|
} catch {
|
|
@@ -5132,6 +5188,7 @@ ${colors_default.bold("How it works:")}
|
|
|
5132
5188
|
5. Frameworks that ignore PORT (Vite, VitePlus, Astro, React Router, Angular,
|
|
5133
5189
|
Expo, React Native) get --port and, when needed, --host flags
|
|
5134
5190
|
injected automatically
|
|
5191
|
+
6. The proxy listens only on 127.0.0.1 and ::1 unless LAN mode is enabled
|
|
5135
5192
|
Elevated proxy processes keep the invoking user's ~/.portless state directory.
|
|
5136
5193
|
|
|
5137
5194
|
${colors_default.bold("HTTP/2 + HTTPS (default):")}
|
|
@@ -5143,6 +5200,8 @@ ${colors_default.bold("HTTP/2 + HTTPS (default):")}
|
|
|
5143
5200
|
${colors_default.bold("LAN mode:")}
|
|
5144
5201
|
Use --lan to make services accessible from other devices (phones,
|
|
5145
5202
|
tablets) on the same WiFi network via mDNS (.local domains).
|
|
5203
|
+
Normal mode binds only to 127.0.0.1 and ::1.
|
|
5204
|
+
LAN mode binds to 0.0.0.0 and ::.
|
|
5146
5205
|
Useful for testing React Native / Expo apps on real devices.
|
|
5147
5206
|
Expo keeps Metro's default LAN host behavior in this mode.
|
|
5148
5207
|
Auto-detected LAN IPs follow network changes automatically.
|
|
@@ -5240,7 +5299,7 @@ ${colors_default.bold("Reserved names:")}
|
|
|
5240
5299
|
process.exit(0);
|
|
5241
5300
|
}
|
|
5242
5301
|
function printVersion() {
|
|
5243
|
-
console.log("0.15.
|
|
5302
|
+
console.log("0.15.4");
|
|
5244
5303
|
process.exit(0);
|
|
5245
5304
|
}
|
|
5246
5305
|
async function handleTrust() {
|
|
@@ -5796,7 +5855,7 @@ ${colors_default.bold("Options:")}
|
|
|
5796
5855
|
const proxyUsesCustomCert = proxyTls && readCustomCertMarker(state.dir);
|
|
5797
5856
|
const stateExists = fs10.existsSync(state.dir);
|
|
5798
5857
|
console.log(colors_default.blue.bold("\nportless doctor\n"));
|
|
5799
|
-
console.log(`Version: ${"0.15.
|
|
5858
|
+
console.log(`Version: ${"0.15.4"}`);
|
|
5800
5859
|
console.log(`Node.js: ${process.versions.node}`);
|
|
5801
5860
|
console.log(`Platform: ${process.platform} ${process.arch}`);
|
|
5802
5861
|
console.log(`State dir: ${state.dir}`);
|
|
@@ -6080,6 +6139,8 @@ ${colors_default.bold("Usage:")}
|
|
|
6080
6139
|
${colors_default.cyan("portless proxy stop")} Stop the proxy
|
|
6081
6140
|
|
|
6082
6141
|
${colors_default.bold("LAN mode (--lan):")}
|
|
6142
|
+
Without LAN mode, the proxy listens only on 127.0.0.1 and ::1.
|
|
6143
|
+
LAN mode explicitly binds the proxy to 0.0.0.0 and ::.
|
|
6083
6144
|
Makes services accessible from other devices on the same WiFi network
|
|
6084
6145
|
via mDNS (.local domains). Useful for testing on real mobile devices.
|
|
6085
6146
|
Auto-detects your LAN IP and follows changes automatically, or use
|