portless 0.4.1 → 0.4.2
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/dist/chunk-JMRTQAVX.js +783 -0
- package/dist/cli.js +98 -306
- package/dist/index.d.ts +24 -3
- package/dist/index.js +9 -1
- package/package.json +13 -12
- package/LICENSE +0 -201
- package/dist/chunk-VRBD6YAY.js +0 -412
package/dist/cli.js
CHANGED
|
@@ -1,19 +1,34 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
FILE_MODE,
|
|
4
|
-
|
|
4
|
+
PRIVILEGED_PORT_THRESHOLD,
|
|
5
|
+
RouteConflictError,
|
|
5
6
|
RouteStore,
|
|
6
7
|
createProxyServer,
|
|
8
|
+
discoverState,
|
|
9
|
+
findFreePort,
|
|
10
|
+
findPidOnPort,
|
|
11
|
+
fixOwnership,
|
|
7
12
|
formatUrl,
|
|
13
|
+
getDefaultPort,
|
|
14
|
+
injectFrameworkFlags,
|
|
8
15
|
isErrnoException,
|
|
9
|
-
|
|
10
|
-
|
|
16
|
+
isHttpsEnvEnabled,
|
|
17
|
+
isProxyRunning,
|
|
18
|
+
parseHostname,
|
|
19
|
+
prompt,
|
|
20
|
+
readTlsMarker,
|
|
21
|
+
resolveStateDir,
|
|
22
|
+
spawnCommand,
|
|
23
|
+
waitForProxy,
|
|
24
|
+
writeTlsMarker
|
|
25
|
+
} from "./chunk-JMRTQAVX.js";
|
|
11
26
|
|
|
12
27
|
// src/cli.ts
|
|
13
28
|
import chalk from "chalk";
|
|
14
|
-
import * as
|
|
15
|
-
import * as
|
|
16
|
-
import { spawn
|
|
29
|
+
import * as fs2 from "fs";
|
|
30
|
+
import * as path2 from "path";
|
|
31
|
+
import { spawn, spawnSync } from "child_process";
|
|
17
32
|
|
|
18
33
|
// src/certs.ts
|
|
19
34
|
import * as fs from "fs";
|
|
@@ -23,17 +38,6 @@ import * as tls from "tls";
|
|
|
23
38
|
import { execFile as execFileCb, execFileSync } from "child_process";
|
|
24
39
|
import { promisify } from "util";
|
|
25
40
|
var CA_VALIDITY_DAYS = 3650;
|
|
26
|
-
function fixOwnership(...paths) {
|
|
27
|
-
const uid = process.env.SUDO_UID;
|
|
28
|
-
const gid = process.env.SUDO_GID;
|
|
29
|
-
if (!uid || process.getuid?.() !== 0) return;
|
|
30
|
-
for (const p of paths) {
|
|
31
|
-
try {
|
|
32
|
-
fs.chownSync(p, parseInt(uid, 10), parseInt(gid || uid, 10));
|
|
33
|
-
} catch {
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
41
|
var SERVER_VALIDITY_DAYS = 365;
|
|
38
42
|
var EXPIRY_BUFFER_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
39
43
|
var CA_COMMON_NAME = "portless Local CA";
|
|
@@ -60,6 +64,17 @@ function isCertValid(certPath) {
|
|
|
60
64
|
return false;
|
|
61
65
|
}
|
|
62
66
|
}
|
|
67
|
+
function isCertSignatureStrong(certPath) {
|
|
68
|
+
try {
|
|
69
|
+
const text = openssl(["x509", "-in", certPath, "-noout", "-text"]);
|
|
70
|
+
const match = text.match(/Signature Algorithm:\s*(\S+)/i);
|
|
71
|
+
if (!match) return false;
|
|
72
|
+
const algo = match[1].toLowerCase();
|
|
73
|
+
return !algo.includes("sha1");
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
63
78
|
function openssl(args, options) {
|
|
64
79
|
try {
|
|
65
80
|
return execFileSync("openssl", args, {
|
|
@@ -102,6 +117,7 @@ function generateCA(stateDir) {
|
|
|
102
117
|
"req",
|
|
103
118
|
"-new",
|
|
104
119
|
"-x509",
|
|
120
|
+
"-sha256",
|
|
105
121
|
"-key",
|
|
106
122
|
keyPath,
|
|
107
123
|
"-out",
|
|
@@ -142,6 +158,7 @@ function generateServerCert(stateDir) {
|
|
|
142
158
|
openssl([
|
|
143
159
|
"x509",
|
|
144
160
|
"-req",
|
|
161
|
+
"-sha256",
|
|
145
162
|
"-in",
|
|
146
163
|
csrPath,
|
|
147
164
|
"-CA",
|
|
@@ -171,12 +188,13 @@ function ensureCerts(stateDir) {
|
|
|
171
188
|
const caCertPath = path.join(stateDir, CA_CERT_FILE);
|
|
172
189
|
const caKeyPath = path.join(stateDir, CA_KEY_FILE);
|
|
173
190
|
const serverCertPath = path.join(stateDir, SERVER_CERT_FILE);
|
|
191
|
+
const serverKeyPath = path.join(stateDir, SERVER_KEY_FILE);
|
|
174
192
|
let caGenerated = false;
|
|
175
|
-
if (!fileExists(caCertPath) || !fileExists(caKeyPath) || !isCertValid(caCertPath)) {
|
|
193
|
+
if (!fileExists(caCertPath) || !fileExists(caKeyPath) || !isCertValid(caCertPath) || !isCertSignatureStrong(caCertPath)) {
|
|
176
194
|
generateCA(stateDir);
|
|
177
195
|
caGenerated = true;
|
|
178
196
|
}
|
|
179
|
-
if (caGenerated || !fileExists(serverCertPath) || !isCertValid(serverCertPath)) {
|
|
197
|
+
if (caGenerated || !fileExists(serverCertPath) || !fileExists(serverKeyPath) || !isCertValid(serverCertPath) || !isCertSignatureStrong(serverCertPath)) {
|
|
180
198
|
generateServerCert(stateDir);
|
|
181
199
|
}
|
|
182
200
|
return {
|
|
@@ -276,6 +294,7 @@ async function generateHostCertAsync(stateDir, hostname) {
|
|
|
276
294
|
await opensslAsync([
|
|
277
295
|
"x509",
|
|
278
296
|
"-req",
|
|
297
|
+
"-sha256",
|
|
279
298
|
"-in",
|
|
280
299
|
csrPath,
|
|
281
300
|
"-CA",
|
|
@@ -301,16 +320,12 @@ async function generateHostCertAsync(stateDir, hostname) {
|
|
|
301
320
|
fixOwnership(keyPath, certPath);
|
|
302
321
|
return { certPath, keyPath };
|
|
303
322
|
}
|
|
304
|
-
function isSimpleLocalhostSubdomain(hostname) {
|
|
305
|
-
const parts = hostname.split(".");
|
|
306
|
-
return parts.length === 2 && parts[1] === "localhost";
|
|
307
|
-
}
|
|
308
323
|
function createSNICallback(stateDir, defaultCert, defaultKey) {
|
|
309
324
|
const cache = /* @__PURE__ */ new Map();
|
|
310
325
|
const pending = /* @__PURE__ */ new Map();
|
|
311
326
|
const defaultCtx = tls.createSecureContext({ cert: defaultCert, key: defaultKey });
|
|
312
327
|
return (servername, cb) => {
|
|
313
|
-
if (servername === "localhost"
|
|
328
|
+
if (servername === "localhost") {
|
|
314
329
|
cb(null, defaultCtx);
|
|
315
330
|
return;
|
|
316
331
|
}
|
|
@@ -322,7 +337,7 @@ function createSNICallback(stateDir, defaultCert, defaultKey) {
|
|
|
322
337
|
const hostDir = path.join(stateDir, HOST_CERTS_DIR);
|
|
323
338
|
const certPath = path.join(hostDir, `${safeName}.pem`);
|
|
324
339
|
const keyPath = path.join(hostDir, `${safeName}-key.pem`);
|
|
325
|
-
if (fileExists(certPath) && fileExists(keyPath) && isCertValid(certPath)) {
|
|
340
|
+
if (fileExists(certPath) && fileExists(keyPath) && isCertValid(certPath) && isCertSignatureStrong(certPath)) {
|
|
326
341
|
try {
|
|
327
342
|
const ctx = tls.createSecureContext({
|
|
328
343
|
cert: fs.readFileSync(certPath),
|
|
@@ -389,252 +404,6 @@ function trustCA(stateDir) {
|
|
|
389
404
|
}
|
|
390
405
|
}
|
|
391
406
|
|
|
392
|
-
// src/cli-utils.ts
|
|
393
|
-
import * as fs2 from "fs";
|
|
394
|
-
import * as http from "http";
|
|
395
|
-
import * as https from "https";
|
|
396
|
-
import * as net from "net";
|
|
397
|
-
import * as os from "os";
|
|
398
|
-
import * as path2 from "path";
|
|
399
|
-
import * as readline from "readline";
|
|
400
|
-
import { execSync, spawn } from "child_process";
|
|
401
|
-
var DEFAULT_PROXY_PORT = 1355;
|
|
402
|
-
var PRIVILEGED_PORT_THRESHOLD = 1024;
|
|
403
|
-
var SYSTEM_STATE_DIR = "/tmp/portless";
|
|
404
|
-
var USER_STATE_DIR = path2.join(os.homedir(), ".portless");
|
|
405
|
-
var MIN_APP_PORT = 4e3;
|
|
406
|
-
var MAX_APP_PORT = 4999;
|
|
407
|
-
var RANDOM_PORT_ATTEMPTS = 50;
|
|
408
|
-
var SOCKET_TIMEOUT_MS = 500;
|
|
409
|
-
var LSOF_TIMEOUT_MS = 5e3;
|
|
410
|
-
var WAIT_FOR_PROXY_MAX_ATTEMPTS = 20;
|
|
411
|
-
var WAIT_FOR_PROXY_INTERVAL_MS = 250;
|
|
412
|
-
var SIGNAL_CODES = {
|
|
413
|
-
SIGHUP: 1,
|
|
414
|
-
SIGINT: 2,
|
|
415
|
-
SIGQUIT: 3,
|
|
416
|
-
SIGABRT: 6,
|
|
417
|
-
SIGKILL: 9,
|
|
418
|
-
SIGTERM: 15
|
|
419
|
-
};
|
|
420
|
-
function getDefaultPort() {
|
|
421
|
-
const envPort = process.env.PORTLESS_PORT;
|
|
422
|
-
if (envPort) {
|
|
423
|
-
const port = parseInt(envPort, 10);
|
|
424
|
-
if (!isNaN(port) && port >= 1 && port <= 65535) return port;
|
|
425
|
-
}
|
|
426
|
-
return DEFAULT_PROXY_PORT;
|
|
427
|
-
}
|
|
428
|
-
function resolveStateDir(port) {
|
|
429
|
-
if (process.env.PORTLESS_STATE_DIR) return process.env.PORTLESS_STATE_DIR;
|
|
430
|
-
return port < PRIVILEGED_PORT_THRESHOLD ? SYSTEM_STATE_DIR : USER_STATE_DIR;
|
|
431
|
-
}
|
|
432
|
-
function readPortFromDir(dir) {
|
|
433
|
-
try {
|
|
434
|
-
const raw = fs2.readFileSync(path2.join(dir, "proxy.port"), "utf-8").trim();
|
|
435
|
-
const port = parseInt(raw, 10);
|
|
436
|
-
return isNaN(port) ? null : port;
|
|
437
|
-
} catch {
|
|
438
|
-
return null;
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
var TLS_MARKER_FILE = "proxy.tls";
|
|
442
|
-
function readTlsMarker(dir) {
|
|
443
|
-
try {
|
|
444
|
-
return fs2.existsSync(path2.join(dir, TLS_MARKER_FILE));
|
|
445
|
-
} catch {
|
|
446
|
-
return false;
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
function writeTlsMarker(dir, enabled) {
|
|
450
|
-
const markerPath = path2.join(dir, TLS_MARKER_FILE);
|
|
451
|
-
if (enabled) {
|
|
452
|
-
fs2.writeFileSync(markerPath, "1", { mode: 420 });
|
|
453
|
-
} else {
|
|
454
|
-
try {
|
|
455
|
-
fs2.unlinkSync(markerPath);
|
|
456
|
-
} catch {
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
function isHttpsEnvEnabled() {
|
|
461
|
-
const val = process.env.PORTLESS_HTTPS;
|
|
462
|
-
return val === "1" || val === "true";
|
|
463
|
-
}
|
|
464
|
-
async function discoverState() {
|
|
465
|
-
if (process.env.PORTLESS_STATE_DIR) {
|
|
466
|
-
const dir = process.env.PORTLESS_STATE_DIR;
|
|
467
|
-
const port = readPortFromDir(dir) ?? getDefaultPort();
|
|
468
|
-
const tls2 = readTlsMarker(dir);
|
|
469
|
-
return { dir, port, tls: tls2 };
|
|
470
|
-
}
|
|
471
|
-
const userPort = readPortFromDir(USER_STATE_DIR);
|
|
472
|
-
if (userPort !== null) {
|
|
473
|
-
const tls2 = readTlsMarker(USER_STATE_DIR);
|
|
474
|
-
if (await isProxyRunning(userPort, tls2)) {
|
|
475
|
-
return { dir: USER_STATE_DIR, port: userPort, tls: tls2 };
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
const systemPort = readPortFromDir(SYSTEM_STATE_DIR);
|
|
479
|
-
if (systemPort !== null) {
|
|
480
|
-
const tls2 = readTlsMarker(SYSTEM_STATE_DIR);
|
|
481
|
-
if (await isProxyRunning(systemPort, tls2)) {
|
|
482
|
-
return { dir: SYSTEM_STATE_DIR, port: systemPort, tls: tls2 };
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
const defaultPort = getDefaultPort();
|
|
486
|
-
return { dir: resolveStateDir(defaultPort), port: defaultPort, tls: false };
|
|
487
|
-
}
|
|
488
|
-
async function findFreePort(minPort = MIN_APP_PORT, maxPort = MAX_APP_PORT) {
|
|
489
|
-
if (minPort > maxPort) {
|
|
490
|
-
throw new Error(`minPort (${minPort}) must be <= maxPort (${maxPort})`);
|
|
491
|
-
}
|
|
492
|
-
const tryPort = (port) => {
|
|
493
|
-
return new Promise((resolve) => {
|
|
494
|
-
const server = net.createServer();
|
|
495
|
-
server.listen(port, () => {
|
|
496
|
-
server.close(() => resolve(true));
|
|
497
|
-
});
|
|
498
|
-
server.on("error", () => resolve(false));
|
|
499
|
-
});
|
|
500
|
-
};
|
|
501
|
-
for (let i = 0; i < RANDOM_PORT_ATTEMPTS; i++) {
|
|
502
|
-
const port = minPort + Math.floor(Math.random() * (maxPort - minPort + 1));
|
|
503
|
-
if (await tryPort(port)) {
|
|
504
|
-
return port;
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
for (let port = minPort; port <= maxPort; port++) {
|
|
508
|
-
if (await tryPort(port)) {
|
|
509
|
-
return port;
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
throw new Error(`No free port found in range ${minPort}-${maxPort}`);
|
|
513
|
-
}
|
|
514
|
-
function isProxyRunning(port, tls2 = false) {
|
|
515
|
-
return new Promise((resolve) => {
|
|
516
|
-
const requestFn = tls2 ? https.request : http.request;
|
|
517
|
-
const req = requestFn(
|
|
518
|
-
{
|
|
519
|
-
hostname: "127.0.0.1",
|
|
520
|
-
port,
|
|
521
|
-
path: "/",
|
|
522
|
-
method: "HEAD",
|
|
523
|
-
timeout: SOCKET_TIMEOUT_MS,
|
|
524
|
-
...tls2 ? { rejectUnauthorized: false } : {}
|
|
525
|
-
},
|
|
526
|
-
(res) => {
|
|
527
|
-
res.resume();
|
|
528
|
-
resolve(res.headers[PORTLESS_HEADER.toLowerCase()] === "1");
|
|
529
|
-
}
|
|
530
|
-
);
|
|
531
|
-
req.on("error", () => resolve(false));
|
|
532
|
-
req.on("timeout", () => {
|
|
533
|
-
req.destroy();
|
|
534
|
-
resolve(false);
|
|
535
|
-
});
|
|
536
|
-
req.end();
|
|
537
|
-
});
|
|
538
|
-
}
|
|
539
|
-
function findPidOnPort(port) {
|
|
540
|
-
try {
|
|
541
|
-
const output = execSync(`lsof -ti tcp:${port} -sTCP:LISTEN`, {
|
|
542
|
-
encoding: "utf-8",
|
|
543
|
-
timeout: LSOF_TIMEOUT_MS
|
|
544
|
-
});
|
|
545
|
-
const pid = parseInt(output.trim().split("\n")[0], 10);
|
|
546
|
-
return isNaN(pid) ? null : pid;
|
|
547
|
-
} catch {
|
|
548
|
-
return null;
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
async function waitForProxy(port, maxAttempts = WAIT_FOR_PROXY_MAX_ATTEMPTS, intervalMs = WAIT_FOR_PROXY_INTERVAL_MS, tls2 = false) {
|
|
552
|
-
for (let i = 0; i < maxAttempts; i++) {
|
|
553
|
-
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
554
|
-
if (await isProxyRunning(port, tls2)) {
|
|
555
|
-
return true;
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
return false;
|
|
559
|
-
}
|
|
560
|
-
function spawnCommand(commandArgs, options) {
|
|
561
|
-
const child = spawn(commandArgs[0], commandArgs.slice(1), {
|
|
562
|
-
stdio: "inherit",
|
|
563
|
-
env: options?.env
|
|
564
|
-
});
|
|
565
|
-
let exiting = false;
|
|
566
|
-
const cleanup = () => {
|
|
567
|
-
process.removeListener("SIGINT", onSigInt);
|
|
568
|
-
process.removeListener("SIGTERM", onSigTerm);
|
|
569
|
-
options?.onCleanup?.();
|
|
570
|
-
};
|
|
571
|
-
const handleSignal = (signal) => {
|
|
572
|
-
if (exiting) return;
|
|
573
|
-
exiting = true;
|
|
574
|
-
child.kill(signal);
|
|
575
|
-
cleanup();
|
|
576
|
-
process.exit(128 + (SIGNAL_CODES[signal] || 15));
|
|
577
|
-
};
|
|
578
|
-
const onSigInt = () => handleSignal("SIGINT");
|
|
579
|
-
const onSigTerm = () => handleSignal("SIGTERM");
|
|
580
|
-
process.on("SIGINT", onSigInt);
|
|
581
|
-
process.on("SIGTERM", onSigTerm);
|
|
582
|
-
child.on("error", (err) => {
|
|
583
|
-
if (exiting) return;
|
|
584
|
-
exiting = true;
|
|
585
|
-
console.error(`Failed to run command: ${err.message}`);
|
|
586
|
-
if (err.code === "ENOENT") {
|
|
587
|
-
console.error(`Is "${commandArgs[0]}" installed and in your PATH?`);
|
|
588
|
-
}
|
|
589
|
-
cleanup();
|
|
590
|
-
process.exit(1);
|
|
591
|
-
});
|
|
592
|
-
child.on("exit", (code, signal) => {
|
|
593
|
-
if (exiting) return;
|
|
594
|
-
exiting = true;
|
|
595
|
-
cleanup();
|
|
596
|
-
if (signal) {
|
|
597
|
-
process.exit(128 + (SIGNAL_CODES[signal] || 15));
|
|
598
|
-
}
|
|
599
|
-
process.exit(code ?? 1);
|
|
600
|
-
});
|
|
601
|
-
}
|
|
602
|
-
var FRAMEWORKS_NEEDING_PORT = {
|
|
603
|
-
vite: { strictPort: true },
|
|
604
|
-
"react-router": { strictPort: true },
|
|
605
|
-
astro: { strictPort: false },
|
|
606
|
-
ng: { strictPort: false }
|
|
607
|
-
};
|
|
608
|
-
function injectFrameworkFlags(commandArgs, port) {
|
|
609
|
-
const cmd = commandArgs[0];
|
|
610
|
-
if (!cmd) return;
|
|
611
|
-
const basename2 = path2.basename(cmd);
|
|
612
|
-
const framework = FRAMEWORKS_NEEDING_PORT[basename2];
|
|
613
|
-
if (!framework) return;
|
|
614
|
-
if (!commandArgs.includes("--port")) {
|
|
615
|
-
commandArgs.push("--port", port.toString());
|
|
616
|
-
if (framework.strictPort) {
|
|
617
|
-
commandArgs.push("--strictPort");
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
if (!commandArgs.includes("--host")) {
|
|
621
|
-
commandArgs.push("--host", "127.0.0.1");
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
function prompt(question) {
|
|
625
|
-
const rl = readline.createInterface({
|
|
626
|
-
input: process.stdin,
|
|
627
|
-
output: process.stdout
|
|
628
|
-
});
|
|
629
|
-
return new Promise((resolve) => {
|
|
630
|
-
rl.on("close", () => resolve(""));
|
|
631
|
-
rl.question(question, (answer) => {
|
|
632
|
-
rl.close();
|
|
633
|
-
resolve(answer.trim().toLowerCase());
|
|
634
|
-
});
|
|
635
|
-
});
|
|
636
|
-
}
|
|
637
|
-
|
|
638
407
|
// src/cli.ts
|
|
639
408
|
var DEBOUNCE_MS = 100;
|
|
640
409
|
var POLL_INTERVAL_MS = 3e3;
|
|
@@ -644,13 +413,14 @@ function startProxyServer(store, proxyPort, tlsOptions) {
|
|
|
644
413
|
store.ensureDir();
|
|
645
414
|
const isTls = !!tlsOptions;
|
|
646
415
|
const routesPath = store.getRoutesPath();
|
|
647
|
-
if (!
|
|
648
|
-
|
|
416
|
+
if (!fs2.existsSync(routesPath)) {
|
|
417
|
+
fs2.writeFileSync(routesPath, "[]", { mode: FILE_MODE });
|
|
649
418
|
}
|
|
650
419
|
try {
|
|
651
|
-
|
|
420
|
+
fs2.chmodSync(routesPath, FILE_MODE);
|
|
652
421
|
} catch {
|
|
653
422
|
}
|
|
423
|
+
fixOwnership(routesPath);
|
|
654
424
|
let cachedRoutes = store.loadRoutes();
|
|
655
425
|
let debounceTimer = null;
|
|
656
426
|
let watcher = null;
|
|
@@ -662,7 +432,7 @@ function startProxyServer(store, proxyPort, tlsOptions) {
|
|
|
662
432
|
}
|
|
663
433
|
};
|
|
664
434
|
try {
|
|
665
|
-
watcher =
|
|
435
|
+
watcher = fs2.watch(routesPath, () => {
|
|
666
436
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
667
437
|
debounceTimer = setTimeout(reloadRoutes, DEBOUNCE_MS);
|
|
668
438
|
});
|
|
@@ -695,9 +465,10 @@ function startProxyServer(store, proxyPort, tlsOptions) {
|
|
|
695
465
|
process.exit(1);
|
|
696
466
|
});
|
|
697
467
|
server.listen(proxyPort, () => {
|
|
698
|
-
|
|
699
|
-
|
|
468
|
+
fs2.writeFileSync(store.pidPath, process.pid.toString(), { mode: FILE_MODE });
|
|
469
|
+
fs2.writeFileSync(store.portFilePath, proxyPort.toString(), { mode: FILE_MODE });
|
|
700
470
|
writeTlsMarker(store.dir, isTls);
|
|
471
|
+
fixOwnership(store.dir, store.pidPath, store.portFilePath);
|
|
701
472
|
const proto = isTls ? "HTTPS/2" : "HTTP";
|
|
702
473
|
console.log(chalk.green(`${proto} proxy listening on port ${proxyPort}`));
|
|
703
474
|
});
|
|
@@ -711,11 +482,11 @@ function startProxyServer(store, proxyPort, tlsOptions) {
|
|
|
711
482
|
watcher.close();
|
|
712
483
|
}
|
|
713
484
|
try {
|
|
714
|
-
|
|
485
|
+
fs2.unlinkSync(store.pidPath);
|
|
715
486
|
} catch {
|
|
716
487
|
}
|
|
717
488
|
try {
|
|
718
|
-
|
|
489
|
+
fs2.unlinkSync(store.portFilePath);
|
|
719
490
|
} catch {
|
|
720
491
|
}
|
|
721
492
|
writeTlsMarker(store.dir, false);
|
|
@@ -731,7 +502,7 @@ async function stopProxy(store, proxyPort, tls2) {
|
|
|
731
502
|
const pidPath = store.pidPath;
|
|
732
503
|
const needsSudo = proxyPort < PRIVILEGED_PORT_THRESHOLD;
|
|
733
504
|
const sudoHint = needsSudo ? "sudo " : "";
|
|
734
|
-
if (!
|
|
505
|
+
if (!fs2.existsSync(pidPath)) {
|
|
735
506
|
if (await isProxyRunning(proxyPort, tls2)) {
|
|
736
507
|
console.log(chalk.yellow(`PID file is missing but port ${proxyPort} is still in use.`));
|
|
737
508
|
const pid = findPidOnPort(proxyPort);
|
|
@@ -739,7 +510,7 @@ async function stopProxy(store, proxyPort, tls2) {
|
|
|
739
510
|
try {
|
|
740
511
|
process.kill(pid, "SIGTERM");
|
|
741
512
|
try {
|
|
742
|
-
|
|
513
|
+
fs2.unlinkSync(store.portFilePath);
|
|
743
514
|
} catch {
|
|
744
515
|
}
|
|
745
516
|
console.log(chalk.green(`Killed process ${pid}. Proxy stopped.`));
|
|
@@ -770,19 +541,19 @@ async function stopProxy(store, proxyPort, tls2) {
|
|
|
770
541
|
return;
|
|
771
542
|
}
|
|
772
543
|
try {
|
|
773
|
-
const pid = parseInt(
|
|
544
|
+
const pid = parseInt(fs2.readFileSync(pidPath, "utf-8"), 10);
|
|
774
545
|
if (isNaN(pid)) {
|
|
775
546
|
console.error(chalk.red("Corrupted PID file. Removing it."));
|
|
776
|
-
|
|
547
|
+
fs2.unlinkSync(pidPath);
|
|
777
548
|
return;
|
|
778
549
|
}
|
|
779
550
|
try {
|
|
780
551
|
process.kill(pid, 0);
|
|
781
552
|
} catch {
|
|
782
553
|
console.log(chalk.yellow("Proxy process is no longer running. Cleaning up stale files."));
|
|
783
|
-
|
|
554
|
+
fs2.unlinkSync(pidPath);
|
|
784
555
|
try {
|
|
785
|
-
|
|
556
|
+
fs2.unlinkSync(store.portFilePath);
|
|
786
557
|
} catch {
|
|
787
558
|
}
|
|
788
559
|
return;
|
|
@@ -794,13 +565,13 @@ async function stopProxy(store, proxyPort, tls2) {
|
|
|
794
565
|
)
|
|
795
566
|
);
|
|
796
567
|
console.log(chalk.yellow("Removing stale PID file."));
|
|
797
|
-
|
|
568
|
+
fs2.unlinkSync(pidPath);
|
|
798
569
|
return;
|
|
799
570
|
}
|
|
800
571
|
process.kill(pid, "SIGTERM");
|
|
801
|
-
|
|
572
|
+
fs2.unlinkSync(pidPath);
|
|
802
573
|
try {
|
|
803
|
-
|
|
574
|
+
fs2.unlinkSync(store.portFilePath);
|
|
804
575
|
} catch {
|
|
805
576
|
}
|
|
806
577
|
console.log(chalk.green("Proxy stopped."));
|
|
@@ -833,7 +604,7 @@ function listRoutes(store, proxyPort, tls2) {
|
|
|
833
604
|
}
|
|
834
605
|
console.log();
|
|
835
606
|
}
|
|
836
|
-
async function runApp(store, proxyPort, stateDir, name, commandArgs, tls2) {
|
|
607
|
+
async function runApp(store, proxyPort, stateDir, name, commandArgs, tls2, force) {
|
|
837
608
|
const hostname = parseHostname(name);
|
|
838
609
|
console.log(chalk.blue.bold(`
|
|
839
610
|
portless
|
|
@@ -893,10 +664,10 @@ portless
|
|
|
893
664
|
const autoTls = readTlsMarker(stateDir);
|
|
894
665
|
if (!await waitForProxy(defaultPort, void 0, void 0, autoTls)) {
|
|
895
666
|
console.error(chalk.red("Proxy failed to start (timed out waiting for it to listen)."));
|
|
896
|
-
const logPath =
|
|
667
|
+
const logPath = path2.join(stateDir, "proxy.log");
|
|
897
668
|
console.error(chalk.blue("Try starting the proxy manually to see the error:"));
|
|
898
669
|
console.error(chalk.cyan(` ${needsSudo ? "sudo " : ""}portless proxy start`));
|
|
899
|
-
if (
|
|
670
|
+
if (fs2.existsSync(logPath)) {
|
|
900
671
|
console.error(chalk.gray(`Logs: ${logPath}`));
|
|
901
672
|
}
|
|
902
673
|
process.exit(1);
|
|
@@ -908,7 +679,15 @@ portless
|
|
|
908
679
|
}
|
|
909
680
|
const port = await findFreePort();
|
|
910
681
|
console.log(chalk.green(`-- Using port ${port}`));
|
|
911
|
-
|
|
682
|
+
try {
|
|
683
|
+
store.addRoute(hostname, port, process.pid, force);
|
|
684
|
+
} catch (err) {
|
|
685
|
+
if (err instanceof RouteConflictError) {
|
|
686
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
687
|
+
process.exit(1);
|
|
688
|
+
}
|
|
689
|
+
throw err;
|
|
690
|
+
}
|
|
912
691
|
const finalUrl = formatUrl(hostname, proxyPort, tls2);
|
|
913
692
|
console.log(chalk.cyan.bold(`
|
|
914
693
|
-> ${finalUrl}
|
|
@@ -932,6 +711,14 @@ portless
|
|
|
932
711
|
});
|
|
933
712
|
}
|
|
934
713
|
async function main() {
|
|
714
|
+
if (process.stdin.isTTY) {
|
|
715
|
+
process.on("exit", () => {
|
|
716
|
+
try {
|
|
717
|
+
process.stdin.setRawMode(false);
|
|
718
|
+
} catch {
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
}
|
|
935
722
|
const args = process.argv.slice(2);
|
|
936
723
|
const isNpx = process.env.npm_command === "exec" && !process.env.npm_lifecycle_event;
|
|
937
724
|
const isPnpmDlx = !!process.env.PNPM_SCRIPT_SRC_DIR && !process.env.npm_lifecycle_event;
|
|
@@ -1001,6 +788,7 @@ ${chalk.bold("Options:")}
|
|
|
1001
788
|
--key <path> Use a custom TLS private key (implies --https)
|
|
1002
789
|
--no-tls Disable HTTPS (overrides PORTLESS_HTTPS)
|
|
1003
790
|
--foreground Run proxy in foreground (for debugging)
|
|
791
|
+
--force Override an existing route registered by another process
|
|
1004
792
|
|
|
1005
793
|
${chalk.bold("Environment variables:")}
|
|
1006
794
|
PORTLESS_PORT=<number> Override the default proxy port (e.g. in .bashrc)
|
|
@@ -1015,7 +803,7 @@ ${chalk.bold("Skip portless:")}
|
|
|
1015
803
|
process.exit(0);
|
|
1016
804
|
}
|
|
1017
805
|
if (args[0] === "--version" || args[0] === "-v") {
|
|
1018
|
-
console.log("0.4.
|
|
806
|
+
console.log("0.4.2");
|
|
1019
807
|
process.exit(0);
|
|
1020
808
|
}
|
|
1021
809
|
if (args[0] === "trust") {
|
|
@@ -1137,8 +925,8 @@ ${chalk.bold("Usage: portless proxy <command>")}
|
|
|
1137
925
|
store2.ensureDir();
|
|
1138
926
|
if (customCertPath && customKeyPath) {
|
|
1139
927
|
try {
|
|
1140
|
-
const cert =
|
|
1141
|
-
const key =
|
|
928
|
+
const cert = fs2.readFileSync(customCertPath);
|
|
929
|
+
const key = fs2.readFileSync(customKeyPath);
|
|
1142
930
|
const certStr = cert.toString("utf-8");
|
|
1143
931
|
const keyStr = key.toString("utf-8");
|
|
1144
932
|
if (!certStr.includes("-----BEGIN CERTIFICATE-----")) {
|
|
@@ -1183,8 +971,8 @@ ${chalk.bold("Usage: portless proxy <command>")}
|
|
|
1183
971
|
console.warn(chalk.cyan(" portless trust"));
|
|
1184
972
|
}
|
|
1185
973
|
}
|
|
1186
|
-
const cert =
|
|
1187
|
-
const key =
|
|
974
|
+
const cert = fs2.readFileSync(certs.certPath);
|
|
975
|
+
const key = fs2.readFileSync(certs.keyPath);
|
|
1188
976
|
tlsOptions = {
|
|
1189
977
|
cert,
|
|
1190
978
|
key,
|
|
@@ -1198,13 +986,14 @@ ${chalk.bold("Usage: portless proxy <command>")}
|
|
|
1198
986
|
return;
|
|
1199
987
|
}
|
|
1200
988
|
store2.ensureDir();
|
|
1201
|
-
const logPath =
|
|
1202
|
-
const logFd =
|
|
989
|
+
const logPath = path2.join(stateDir, "proxy.log");
|
|
990
|
+
const logFd = fs2.openSync(logPath, "a");
|
|
1203
991
|
try {
|
|
1204
992
|
try {
|
|
1205
|
-
|
|
993
|
+
fs2.chmodSync(logPath, FILE_MODE);
|
|
1206
994
|
} catch {
|
|
1207
995
|
}
|
|
996
|
+
fixOwnership(logPath);
|
|
1208
997
|
const daemonArgs = [process.argv[1], "proxy", "start", "--foreground"];
|
|
1209
998
|
if (portFlagIndex !== -1) {
|
|
1210
999
|
daemonArgs.push("--port", proxyPort.toString());
|
|
@@ -1216,21 +1005,21 @@ ${chalk.bold("Usage: portless proxy <command>")}
|
|
|
1216
1005
|
daemonArgs.push("--https");
|
|
1217
1006
|
}
|
|
1218
1007
|
}
|
|
1219
|
-
const child =
|
|
1008
|
+
const child = spawn(process.execPath, daemonArgs, {
|
|
1220
1009
|
detached: true,
|
|
1221
1010
|
stdio: ["ignore", logFd, logFd],
|
|
1222
1011
|
env: process.env
|
|
1223
1012
|
});
|
|
1224
1013
|
child.unref();
|
|
1225
1014
|
} finally {
|
|
1226
|
-
|
|
1015
|
+
fs2.closeSync(logFd);
|
|
1227
1016
|
}
|
|
1228
1017
|
if (!await waitForProxy(proxyPort, void 0, void 0, useHttps)) {
|
|
1229
1018
|
console.error(chalk.red("Proxy failed to start (timed out waiting for it to listen)."));
|
|
1230
1019
|
console.error(chalk.blue("Try starting the proxy in the foreground to see the error:"));
|
|
1231
1020
|
const needsSudo = proxyPort < PRIVILEGED_PORT_THRESHOLD;
|
|
1232
1021
|
console.error(chalk.cyan(` ${needsSudo ? "sudo " : ""}portless proxy start --foreground`));
|
|
1233
|
-
if (
|
|
1022
|
+
if (fs2.existsSync(logPath)) {
|
|
1234
1023
|
console.error(chalk.gray(`Logs: ${logPath}`));
|
|
1235
1024
|
}
|
|
1236
1025
|
process.exit(1);
|
|
@@ -1239,8 +1028,11 @@ ${chalk.bold("Usage: portless proxy <command>")}
|
|
|
1239
1028
|
console.log(chalk.green(`${proto} proxy started on port ${proxyPort}`));
|
|
1240
1029
|
return;
|
|
1241
1030
|
}
|
|
1242
|
-
const
|
|
1243
|
-
const
|
|
1031
|
+
const forceIdx = args.indexOf("--force");
|
|
1032
|
+
const force = forceIdx >= 0 && forceIdx <= 1;
|
|
1033
|
+
const appArgs = force ? [...args.slice(0, forceIdx), ...args.slice(forceIdx + 1)] : args;
|
|
1034
|
+
const name = appArgs[0];
|
|
1035
|
+
const commandArgs = appArgs.slice(1);
|
|
1244
1036
|
if (commandArgs.length === 0) {
|
|
1245
1037
|
console.error(chalk.red("Error: No command provided."));
|
|
1246
1038
|
console.error(chalk.blue("Usage:"));
|
|
@@ -1253,7 +1045,7 @@ ${chalk.bold("Usage: portless proxy <command>")}
|
|
|
1253
1045
|
const store = new RouteStore(dir, {
|
|
1254
1046
|
onWarning: (msg) => console.warn(chalk.yellow(msg))
|
|
1255
1047
|
});
|
|
1256
|
-
await runApp(store, port, dir, name, commandArgs, tls2);
|
|
1048
|
+
await runApp(store, port, dir, name, commandArgs, tls2, force);
|
|
1257
1049
|
}
|
|
1258
1050
|
main().catch((err) => {
|
|
1259
1051
|
const message = err instanceof Error ? err.message : String(err);
|