portless 0.5.2 → 0.7.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/LICENSE +201 -0
- package/README.md +104 -130
- package/dist/{chunk-P3DHZHEZ.js → chunk-Y6FWHU6F.js} +158 -39
- package/dist/cli.js +360 -95
- package/dist/index.d.ts +14 -10
- package/dist/index.js +3 -3
- package/package.json +14 -14
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/utils.ts
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
function fixOwnership(...paths) {
|
|
4
|
+
if (process.platform === "win32") return;
|
|
4
5
|
const uid = process.env.SUDO_UID;
|
|
5
6
|
const gid = process.env.SUDO_GID;
|
|
6
7
|
if (!uid || process.getuid?.() !== 0) return;
|
|
@@ -22,15 +23,19 @@ function formatUrl(hostname, proxyPort, tls = false) {
|
|
|
22
23
|
const defaultPort = tls ? 443 : 80;
|
|
23
24
|
return proxyPort === defaultPort ? `${proto}://${hostname}` : `${proto}://${hostname}:${proxyPort}`;
|
|
24
25
|
}
|
|
25
|
-
function parseHostname(input) {
|
|
26
|
+
function parseHostname(input, tld = "localhost") {
|
|
27
|
+
const suffix = `.${tld}`;
|
|
26
28
|
let hostname = input.trim().replace(/^https?:\/\//, "").split("/")[0].toLowerCase();
|
|
27
|
-
if (
|
|
29
|
+
if (tld !== "localhost" && hostname.endsWith(".localhost")) {
|
|
30
|
+
hostname = hostname.slice(0, -".localhost".length);
|
|
31
|
+
}
|
|
32
|
+
if (!hostname || hostname === suffix) {
|
|
28
33
|
throw new Error("Hostname cannot be empty");
|
|
29
34
|
}
|
|
30
|
-
if (!hostname.endsWith(
|
|
31
|
-
hostname = `${hostname}
|
|
35
|
+
if (!hostname.endsWith(suffix)) {
|
|
36
|
+
hostname = `${hostname}${suffix}`;
|
|
32
37
|
}
|
|
33
|
-
const name = hostname.
|
|
38
|
+
const name = hostname.slice(0, -suffix.length);
|
|
34
39
|
if (name.includes("..")) {
|
|
35
40
|
throw new Error(`Invalid hostname "${name}": consecutive dots are not allowed`);
|
|
36
41
|
}
|
|
@@ -39,6 +44,14 @@ function parseHostname(input) {
|
|
|
39
44
|
`Invalid hostname "${name}": must contain only lowercase letters, digits, hyphens, and dots`
|
|
40
45
|
);
|
|
41
46
|
}
|
|
47
|
+
const labels = name.split(".");
|
|
48
|
+
for (const label of labels) {
|
|
49
|
+
if (label.length > 63) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`Invalid hostname "${name}": label "${label}" exceeds 63-character DNS limit`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
42
55
|
return hostname;
|
|
43
56
|
}
|
|
44
57
|
|
|
@@ -291,7 +304,14 @@ function findRoute(routes, host) {
|
|
|
291
304
|
return routes.find((r) => r.hostname === host) || routes.find((r) => host.endsWith("." + r.hostname));
|
|
292
305
|
}
|
|
293
306
|
function createProxyServer(options) {
|
|
294
|
-
const {
|
|
307
|
+
const {
|
|
308
|
+
getRoutes,
|
|
309
|
+
proxyPort,
|
|
310
|
+
tld = "localhost",
|
|
311
|
+
onError = (msg) => console.error(msg),
|
|
312
|
+
tls
|
|
313
|
+
} = options;
|
|
314
|
+
const tldSuffix = `.${tld}`;
|
|
295
315
|
const isTls = !!tls;
|
|
296
316
|
const handleRequest = (req, res) => {
|
|
297
317
|
res.setHeader(PORTLESS_HEADER, "1");
|
|
@@ -314,7 +334,7 @@ function createProxyServer(options) {
|
|
|
314
334
|
"Loop Detected",
|
|
315
335
|
`<div class="content"><p class="desc">This request has passed through portless ${hops} times. This usually means a dev server (Vite, webpack, etc.) is proxying requests back through portless without rewriting the Host header.</p><div class="section"><p class="label">Fix: add changeOrigin to your proxy config</p><pre class="terminal">proxy: {
|
|
316
336
|
"/api": {
|
|
317
|
-
target: "http://<backend>
|
|
337
|
+
target: "http://<backend>${escapeHtml(tldSuffix)}:<port>",
|
|
318
338
|
changeOrigin: true,
|
|
319
339
|
},
|
|
320
340
|
}</pre></div></div>`
|
|
@@ -325,13 +345,15 @@ function createProxyServer(options) {
|
|
|
325
345
|
const route = findRoute(routes, host);
|
|
326
346
|
if (!route) {
|
|
327
347
|
const safeHost = escapeHtml(host);
|
|
328
|
-
const
|
|
348
|
+
const strippedHost = host.endsWith(tldSuffix) ? host.slice(0, -tldSuffix.length) : host;
|
|
349
|
+
const safeSuggestion = escapeHtml(strippedHost);
|
|
350
|
+
const routesList = routes.length > 0 ? `<div class="section"><p class="label">Active apps</p><ul class="card">${routes.map((r) => `<li><a href="${escapeHtml(formatUrl(r.hostname, proxyPort, isTls))}" class="card-link"><span class="name">${escapeHtml(r.hostname)}</span><span class="meta"><code class="port">127.0.0.1:${escapeHtml(String(r.port))}</code><span class="arrow">${ARROW_SVG}</span></span></a></li>`).join("")}</ul></div>` : '<p class="empty">No apps running.</p>';
|
|
329
351
|
res.writeHead(404, { "Content-Type": "text/html" });
|
|
330
352
|
res.end(
|
|
331
353
|
renderPage(
|
|
332
354
|
404,
|
|
333
355
|
"Not Found",
|
|
334
|
-
`<div class="content"><p class="desc">No app registered for <strong>${safeHost}</strong></p>${routesList}<div class="section"><div class="terminal"><span class="prompt">$ </span>portless ${
|
|
356
|
+
`<div class="content"><p class="desc">No app registered for <strong>${safeHost}</strong></p>${routesList}<div class="section"><div class="terminal"><span class="prompt">$ </span>portless ${safeSuggestion} your-command</div></div></div>`
|
|
335
357
|
)
|
|
336
358
|
);
|
|
337
359
|
return;
|
|
@@ -523,7 +545,9 @@ function createProxyServer(options) {
|
|
|
523
545
|
// src/hosts.ts
|
|
524
546
|
import * as fs2 from "fs";
|
|
525
547
|
import * as dns from "dns";
|
|
526
|
-
|
|
548
|
+
import * as path from "path";
|
|
549
|
+
var isWindows = process.platform === "win32";
|
|
550
|
+
var HOSTS_PATH = isWindows ? path.join(process.env.SystemRoot ?? "C:\\Windows", "System32", "drivers", "etc", "hosts") : "/etc/hosts";
|
|
527
551
|
var MARKER_START = "# portless-start";
|
|
528
552
|
var MARKER_END = "# portless-end";
|
|
529
553
|
function readHostsFile() {
|
|
@@ -587,7 +611,7 @@ function getManagedHostnames() {
|
|
|
587
611
|
return parts.length >= 2 ? parts[1] : "";
|
|
588
612
|
}).filter(Boolean);
|
|
589
613
|
}
|
|
590
|
-
function
|
|
614
|
+
function checkHostResolution(hostname) {
|
|
591
615
|
return new Promise((resolve) => {
|
|
592
616
|
dns.lookup(hostname, { family: 4 }, (err, address) => {
|
|
593
617
|
if (err) {
|
|
@@ -605,18 +629,19 @@ import * as http3 from "http";
|
|
|
605
629
|
import * as https from "https";
|
|
606
630
|
import * as net2 from "net";
|
|
607
631
|
import * as os from "os";
|
|
608
|
-
import * as
|
|
632
|
+
import * as path2 from "path";
|
|
609
633
|
import * as readline from "readline";
|
|
610
634
|
import { execSync, spawn } from "child_process";
|
|
635
|
+
var isWindows2 = process.platform === "win32";
|
|
611
636
|
var DEFAULT_PROXY_PORT = 1355;
|
|
612
637
|
var PRIVILEGED_PORT_THRESHOLD = 1024;
|
|
613
|
-
var SYSTEM_STATE_DIR = "
|
|
614
|
-
var USER_STATE_DIR =
|
|
638
|
+
var SYSTEM_STATE_DIR = path2.join(os.tmpdir(), "portless");
|
|
639
|
+
var USER_STATE_DIR = path2.join(os.homedir(), ".portless");
|
|
615
640
|
var MIN_APP_PORT = 4e3;
|
|
616
641
|
var MAX_APP_PORT = 4999;
|
|
617
642
|
var RANDOM_PORT_ATTEMPTS = 50;
|
|
618
643
|
var SOCKET_TIMEOUT_MS = 500;
|
|
619
|
-
var
|
|
644
|
+
var PID_LOOKUP_TIMEOUT_MS = 5e3;
|
|
620
645
|
var WAIT_FOR_PROXY_MAX_ATTEMPTS = 20;
|
|
621
646
|
var WAIT_FOR_PROXY_INTERVAL_MS = 250;
|
|
622
647
|
var SIGNAL_CODES = {
|
|
@@ -637,11 +662,12 @@ function getDefaultPort() {
|
|
|
637
662
|
}
|
|
638
663
|
function resolveStateDir(port) {
|
|
639
664
|
if (process.env.PORTLESS_STATE_DIR) return process.env.PORTLESS_STATE_DIR;
|
|
665
|
+
if (isWindows2) return USER_STATE_DIR;
|
|
640
666
|
return port < PRIVILEGED_PORT_THRESHOLD ? SYSTEM_STATE_DIR : USER_STATE_DIR;
|
|
641
667
|
}
|
|
642
668
|
function readPortFromDir(dir) {
|
|
643
669
|
try {
|
|
644
|
-
const raw = fs3.readFileSync(
|
|
670
|
+
const raw = fs3.readFileSync(path2.join(dir, "proxy.port"), "utf-8").trim();
|
|
645
671
|
const port = parseInt(raw, 10);
|
|
646
672
|
return isNaN(port) ? null : port;
|
|
647
673
|
} catch {
|
|
@@ -651,13 +677,13 @@ function readPortFromDir(dir) {
|
|
|
651
677
|
var TLS_MARKER_FILE = "proxy.tls";
|
|
652
678
|
function readTlsMarker(dir) {
|
|
653
679
|
try {
|
|
654
|
-
return fs3.existsSync(
|
|
680
|
+
return fs3.existsSync(path2.join(dir, TLS_MARKER_FILE));
|
|
655
681
|
} catch {
|
|
656
682
|
return false;
|
|
657
683
|
}
|
|
658
684
|
}
|
|
659
685
|
function writeTlsMarker(dir, enabled) {
|
|
660
|
-
const markerPath =
|
|
686
|
+
const markerPath = path2.join(dir, TLS_MARKER_FILE);
|
|
661
687
|
if (enabled) {
|
|
662
688
|
fs3.writeFileSync(markerPath, "1", { mode: 420 });
|
|
663
689
|
} else {
|
|
@@ -667,6 +693,54 @@ function writeTlsMarker(dir, enabled) {
|
|
|
667
693
|
}
|
|
668
694
|
}
|
|
669
695
|
}
|
|
696
|
+
var DEFAULT_TLD = "localhost";
|
|
697
|
+
var RISKY_TLDS = /* @__PURE__ */ new Map([
|
|
698
|
+
["local", "conflicts with mDNS/Bonjour on macOS"],
|
|
699
|
+
["dev", "Google-owned; browsers force HTTPS via preloaded HSTS"],
|
|
700
|
+
["com", "public TLD -- DNS requests will leak to the internet"],
|
|
701
|
+
["org", "public TLD -- DNS requests will leak to the internet"],
|
|
702
|
+
["net", "public TLD -- DNS requests will leak to the internet"],
|
|
703
|
+
["io", "public TLD -- DNS requests will leak to the internet"],
|
|
704
|
+
["app", "public TLD -- DNS requests will leak to the internet"],
|
|
705
|
+
["edu", "public TLD -- DNS requests will leak to the internet"],
|
|
706
|
+
["gov", "public TLD -- DNS requests will leak to the internet"],
|
|
707
|
+
["mil", "public TLD -- DNS requests will leak to the internet"],
|
|
708
|
+
["int", "public TLD -- DNS requests will leak to the internet"]
|
|
709
|
+
]);
|
|
710
|
+
function validateTld(tld) {
|
|
711
|
+
if (!tld) return "TLD cannot be empty";
|
|
712
|
+
if (!/^[a-z0-9]+$/.test(tld)) {
|
|
713
|
+
return `Invalid TLD "${tld}": must contain only lowercase letters and digits`;
|
|
714
|
+
}
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
var TLD_FILE = "proxy.tld";
|
|
718
|
+
function readTldFromDir(dir) {
|
|
719
|
+
try {
|
|
720
|
+
const raw = fs3.readFileSync(path2.join(dir, TLD_FILE), "utf-8").trim();
|
|
721
|
+
return raw || DEFAULT_TLD;
|
|
722
|
+
} catch {
|
|
723
|
+
return DEFAULT_TLD;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
function writeTldFile(dir, tld) {
|
|
727
|
+
const filePath = path2.join(dir, TLD_FILE);
|
|
728
|
+
if (tld === DEFAULT_TLD) {
|
|
729
|
+
try {
|
|
730
|
+
fs3.unlinkSync(filePath);
|
|
731
|
+
} catch {
|
|
732
|
+
}
|
|
733
|
+
} else {
|
|
734
|
+
fs3.writeFileSync(filePath, tld, { mode: 420 });
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
function getDefaultTld() {
|
|
738
|
+
const val = process.env.PORTLESS_TLD?.trim().toLowerCase();
|
|
739
|
+
if (!val) return DEFAULT_TLD;
|
|
740
|
+
const err = validateTld(val);
|
|
741
|
+
if (err) throw new Error(`PORTLESS_TLD: ${err}`);
|
|
742
|
+
return val;
|
|
743
|
+
}
|
|
670
744
|
function isHttpsEnvEnabled() {
|
|
671
745
|
const val = process.env.PORTLESS_HTTPS;
|
|
672
746
|
return val === "1" || val === "true";
|
|
@@ -676,24 +750,36 @@ async function discoverState() {
|
|
|
676
750
|
const dir = process.env.PORTLESS_STATE_DIR;
|
|
677
751
|
const port = readPortFromDir(dir) ?? getDefaultPort();
|
|
678
752
|
const tls = readTlsMarker(dir);
|
|
679
|
-
|
|
753
|
+
const tld = readTldFromDir(dir);
|
|
754
|
+
return { dir, port, tls, tld };
|
|
680
755
|
}
|
|
681
756
|
const userPort = readPortFromDir(USER_STATE_DIR);
|
|
682
757
|
if (userPort !== null) {
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
758
|
+
if (await isProxyRunning(userPort)) {
|
|
759
|
+
const tls = readTlsMarker(USER_STATE_DIR);
|
|
760
|
+
const tld = readTldFromDir(USER_STATE_DIR);
|
|
761
|
+
return { dir: USER_STATE_DIR, port: userPort, tls, tld };
|
|
686
762
|
}
|
|
687
763
|
}
|
|
688
764
|
const systemPort = readPortFromDir(SYSTEM_STATE_DIR);
|
|
689
765
|
if (systemPort !== null) {
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
766
|
+
if (await isProxyRunning(systemPort)) {
|
|
767
|
+
const tls = readTlsMarker(SYSTEM_STATE_DIR);
|
|
768
|
+
const tld = readTldFromDir(SYSTEM_STATE_DIR);
|
|
769
|
+
return { dir: SYSTEM_STATE_DIR, port: systemPort, tls, tld };
|
|
693
770
|
}
|
|
694
771
|
}
|
|
695
772
|
const defaultPort = getDefaultPort();
|
|
696
|
-
|
|
773
|
+
const probePorts = /* @__PURE__ */ new Set([defaultPort, 443, 80]);
|
|
774
|
+
for (const port of probePorts) {
|
|
775
|
+
if (await isProxyRunning(port)) {
|
|
776
|
+
const dir = resolveStateDir(port);
|
|
777
|
+
const tls = readTlsMarker(dir);
|
|
778
|
+
const tld = readTldFromDir(dir);
|
|
779
|
+
return { dir, port, tls, tld };
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
return { dir: resolveStateDir(defaultPort), port: defaultPort, tls: false, tld: getDefaultTld() };
|
|
697
783
|
}
|
|
698
784
|
async function findFreePort(minPort = MIN_APP_PORT, maxPort = MAX_APP_PORT) {
|
|
699
785
|
if (minPort > maxPort) {
|
|
@@ -746,11 +832,34 @@ function isProxyRunning(port, tls = false) {
|
|
|
746
832
|
req.end();
|
|
747
833
|
});
|
|
748
834
|
}
|
|
835
|
+
function parsePidFromNetstat(output, port) {
|
|
836
|
+
for (const line of output.split(/\r?\n/)) {
|
|
837
|
+
if (!line.includes("LISTENING")) continue;
|
|
838
|
+
const parts = line.trim().split(/\s+/);
|
|
839
|
+
if (parts.length < 5) continue;
|
|
840
|
+
const localAddr = parts[1];
|
|
841
|
+
const lastColon = localAddr.lastIndexOf(":");
|
|
842
|
+
if (lastColon === -1) continue;
|
|
843
|
+
const addrPort = parseInt(localAddr.substring(lastColon + 1), 10);
|
|
844
|
+
if (addrPort === port) {
|
|
845
|
+
const pid = parseInt(parts[parts.length - 1], 10);
|
|
846
|
+
if (!isNaN(pid) && pid > 0) return pid;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
return null;
|
|
850
|
+
}
|
|
749
851
|
function findPidOnPort(port) {
|
|
750
852
|
try {
|
|
853
|
+
if (isWindows2) {
|
|
854
|
+
const output2 = execSync("netstat -ano -p tcp", {
|
|
855
|
+
encoding: "utf-8",
|
|
856
|
+
timeout: PID_LOOKUP_TIMEOUT_MS
|
|
857
|
+
});
|
|
858
|
+
return parsePidFromNetstat(output2, port);
|
|
859
|
+
}
|
|
751
860
|
const output = execSync(`lsof -ti tcp:${port} -sTCP:LISTEN`, {
|
|
752
861
|
encoding: "utf-8",
|
|
753
|
-
timeout:
|
|
862
|
+
timeout: PID_LOOKUP_TIMEOUT_MS
|
|
754
863
|
});
|
|
755
864
|
const pid = parseInt(output.trim().split("\n")[0], 10);
|
|
756
865
|
return isNaN(pid) ? null : pid;
|
|
@@ -774,11 +883,11 @@ function collectBinPaths(cwd) {
|
|
|
774
883
|
const dirs = [];
|
|
775
884
|
let dir = cwd;
|
|
776
885
|
for (; ; ) {
|
|
777
|
-
const bin =
|
|
886
|
+
const bin = path2.join(dir, "node_modules", ".bin");
|
|
778
887
|
if (fs3.existsSync(bin)) {
|
|
779
888
|
dirs.push(bin);
|
|
780
889
|
}
|
|
781
|
-
const parent =
|
|
890
|
+
const parent = path2.dirname(dir);
|
|
782
891
|
if (parent === dir) break;
|
|
783
892
|
dir = parent;
|
|
784
893
|
}
|
|
@@ -787,12 +896,15 @@ function collectBinPaths(cwd) {
|
|
|
787
896
|
function augmentedPath(env) {
|
|
788
897
|
const base = (env ?? process.env).PATH ?? "";
|
|
789
898
|
const bins = collectBinPaths(process.cwd());
|
|
790
|
-
return bins.length > 0 ? bins.join(
|
|
899
|
+
return bins.length > 0 ? bins.join(path2.delimiter) + path2.delimiter + base : base;
|
|
791
900
|
}
|
|
792
901
|
function spawnCommand(commandArgs, options) {
|
|
793
902
|
const env = { ...options?.env ?? process.env, PATH: augmentedPath(options?.env) };
|
|
794
|
-
const
|
|
795
|
-
|
|
903
|
+
const child = isWindows2 ? spawn(commandArgs[0], commandArgs.slice(1), {
|
|
904
|
+
stdio: "inherit",
|
|
905
|
+
env,
|
|
906
|
+
shell: true
|
|
907
|
+
}) : spawn("/bin/sh", ["-c", commandArgs.map(shellEscape).join(" ")], {
|
|
796
908
|
stdio: "inherit",
|
|
797
909
|
env
|
|
798
910
|
});
|
|
@@ -844,7 +956,7 @@ var FRAMEWORKS_NEEDING_PORT = {
|
|
|
844
956
|
function injectFrameworkFlags(commandArgs, port) {
|
|
845
957
|
const cmd = commandArgs[0];
|
|
846
958
|
if (!cmd) return;
|
|
847
|
-
const basename2 =
|
|
959
|
+
const basename2 = path2.basename(cmd);
|
|
848
960
|
const framework = FRAMEWORKS_NEEDING_PORT[basename2];
|
|
849
961
|
if (!framework) return;
|
|
850
962
|
if (!commandArgs.includes("--port")) {
|
|
@@ -874,7 +986,7 @@ function prompt(question) {
|
|
|
874
986
|
|
|
875
987
|
// src/routes.ts
|
|
876
988
|
import * as fs4 from "fs";
|
|
877
|
-
import * as
|
|
989
|
+
import * as path3 from "path";
|
|
878
990
|
var STALE_LOCK_THRESHOLD_MS = 1e4;
|
|
879
991
|
var LOCK_MAX_RETRIES = 20;
|
|
880
992
|
var LOCK_RETRY_DELAY_MS = 50;
|
|
@@ -907,10 +1019,10 @@ var RouteStore = class _RouteStore {
|
|
|
907
1019
|
onWarning;
|
|
908
1020
|
constructor(dir, options) {
|
|
909
1021
|
this.dir = dir;
|
|
910
|
-
this.routesPath =
|
|
911
|
-
this.lockPath =
|
|
912
|
-
this.pidPath =
|
|
913
|
-
this.portFilePath =
|
|
1022
|
+
this.routesPath = path3.join(dir, "routes.json");
|
|
1023
|
+
this.lockPath = path3.join(dir, "routes.lock");
|
|
1024
|
+
this.pidPath = path3.join(dir, "proxy.pid");
|
|
1025
|
+
this.portFilePath = path3.join(dir, "proxy.port");
|
|
914
1026
|
this.onWarning = options?.onWarning;
|
|
915
1027
|
}
|
|
916
1028
|
isSystemDir() {
|
|
@@ -1067,12 +1179,19 @@ export {
|
|
|
1067
1179
|
syncHostsFile,
|
|
1068
1180
|
cleanHostsFile,
|
|
1069
1181
|
getManagedHostnames,
|
|
1070
|
-
|
|
1182
|
+
checkHostResolution,
|
|
1183
|
+
isWindows2 as isWindows,
|
|
1071
1184
|
PRIVILEGED_PORT_THRESHOLD,
|
|
1072
1185
|
getDefaultPort,
|
|
1073
1186
|
resolveStateDir,
|
|
1074
1187
|
readTlsMarker,
|
|
1075
1188
|
writeTlsMarker,
|
|
1189
|
+
DEFAULT_TLD,
|
|
1190
|
+
RISKY_TLDS,
|
|
1191
|
+
validateTld,
|
|
1192
|
+
readTldFromDir,
|
|
1193
|
+
writeTldFile,
|
|
1194
|
+
getDefaultTld,
|
|
1076
1195
|
isHttpsEnvEnabled,
|
|
1077
1196
|
discoverState,
|
|
1078
1197
|
findFreePort,
|