portless 0.2.2 → 0.4.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 +74 -30
- package/dist/chunk-VRBD6YAY.js +412 -0
- package/dist/cli.js +880 -165
- package/dist/index.d.ts +36 -5
- package/dist/index.js +9 -1
- package/package.json +2 -4
- package/dist/chunk-SE7KL62V.js +0 -247
package/dist/cli.js
CHANGED
|
@@ -1,21 +1,494 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
FILE_MODE,
|
|
4
|
+
PORTLESS_HEADER,
|
|
3
5
|
RouteStore,
|
|
4
6
|
createProxyServer,
|
|
7
|
+
formatUrl,
|
|
5
8
|
isErrnoException,
|
|
6
9
|
parseHostname
|
|
7
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-VRBD6YAY.js";
|
|
8
11
|
|
|
9
12
|
// src/cli.ts
|
|
10
13
|
import chalk from "chalk";
|
|
14
|
+
import * as fs3 from "fs";
|
|
15
|
+
import * as path3 from "path";
|
|
16
|
+
import { spawn as spawn2, spawnSync } from "child_process";
|
|
17
|
+
|
|
18
|
+
// src/certs.ts
|
|
11
19
|
import * as fs from "fs";
|
|
12
20
|
import * as path from "path";
|
|
13
|
-
import * as
|
|
14
|
-
import
|
|
21
|
+
import * as crypto from "crypto";
|
|
22
|
+
import * as tls from "tls";
|
|
23
|
+
import { execFile as execFileCb, execFileSync } from "child_process";
|
|
24
|
+
import { promisify } from "util";
|
|
25
|
+
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
|
+
var SERVER_VALIDITY_DAYS = 365;
|
|
38
|
+
var EXPIRY_BUFFER_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
39
|
+
var CA_COMMON_NAME = "portless Local CA";
|
|
40
|
+
var OPENSSL_TIMEOUT_MS = 15e3;
|
|
41
|
+
var CA_KEY_FILE = "ca-key.pem";
|
|
42
|
+
var CA_CERT_FILE = "ca.pem";
|
|
43
|
+
var SERVER_KEY_FILE = "server-key.pem";
|
|
44
|
+
var SERVER_CERT_FILE = "server.pem";
|
|
45
|
+
function fileExists(filePath) {
|
|
46
|
+
try {
|
|
47
|
+
fs.accessSync(filePath, fs.constants.R_OK);
|
|
48
|
+
return true;
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function isCertValid(certPath) {
|
|
54
|
+
try {
|
|
55
|
+
const pem = fs.readFileSync(certPath, "utf-8");
|
|
56
|
+
const cert = new crypto.X509Certificate(pem);
|
|
57
|
+
const expiry = new Date(cert.validTo).getTime();
|
|
58
|
+
return Date.now() + EXPIRY_BUFFER_MS < expiry;
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function openssl(args, options) {
|
|
64
|
+
try {
|
|
65
|
+
return execFileSync("openssl", args, {
|
|
66
|
+
encoding: "utf-8",
|
|
67
|
+
timeout: OPENSSL_TIMEOUT_MS,
|
|
68
|
+
input: options?.input,
|
|
69
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
70
|
+
});
|
|
71
|
+
} catch (err) {
|
|
72
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
73
|
+
throw new Error(
|
|
74
|
+
`openssl failed: ${message}
|
|
75
|
+
|
|
76
|
+
Make sure openssl is installed (ships with macOS and most Linux distributions).`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
var execFileAsync = promisify(execFileCb);
|
|
81
|
+
async function opensslAsync(args) {
|
|
82
|
+
try {
|
|
83
|
+
const { stdout } = await execFileAsync("openssl", args, {
|
|
84
|
+
encoding: "utf-8",
|
|
85
|
+
timeout: OPENSSL_TIMEOUT_MS
|
|
86
|
+
});
|
|
87
|
+
return stdout;
|
|
88
|
+
} catch (err) {
|
|
89
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
90
|
+
throw new Error(
|
|
91
|
+
`openssl failed: ${message}
|
|
92
|
+
|
|
93
|
+
Make sure openssl is installed (ships with macOS and most Linux distributions).`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function generateCA(stateDir) {
|
|
98
|
+
const keyPath = path.join(stateDir, CA_KEY_FILE);
|
|
99
|
+
const certPath = path.join(stateDir, CA_CERT_FILE);
|
|
100
|
+
openssl(["ecparam", "-genkey", "-name", "prime256v1", "-noout", "-out", keyPath]);
|
|
101
|
+
openssl([
|
|
102
|
+
"req",
|
|
103
|
+
"-new",
|
|
104
|
+
"-x509",
|
|
105
|
+
"-key",
|
|
106
|
+
keyPath,
|
|
107
|
+
"-out",
|
|
108
|
+
certPath,
|
|
109
|
+
"-days",
|
|
110
|
+
CA_VALIDITY_DAYS.toString(),
|
|
111
|
+
"-subj",
|
|
112
|
+
`/CN=${CA_COMMON_NAME}`,
|
|
113
|
+
"-addext",
|
|
114
|
+
"basicConstraints=critical,CA:TRUE",
|
|
115
|
+
"-addext",
|
|
116
|
+
"keyUsage=critical,keyCertSign,cRLSign"
|
|
117
|
+
]);
|
|
118
|
+
fs.chmodSync(keyPath, 384);
|
|
119
|
+
fs.chmodSync(certPath, 420);
|
|
120
|
+
fixOwnership(keyPath, certPath);
|
|
121
|
+
return { certPath, keyPath };
|
|
122
|
+
}
|
|
123
|
+
function generateServerCert(stateDir) {
|
|
124
|
+
const caKeyPath = path.join(stateDir, CA_KEY_FILE);
|
|
125
|
+
const caCertPath = path.join(stateDir, CA_CERT_FILE);
|
|
126
|
+
const serverKeyPath = path.join(stateDir, SERVER_KEY_FILE);
|
|
127
|
+
const serverCertPath = path.join(stateDir, SERVER_CERT_FILE);
|
|
128
|
+
const csrPath = path.join(stateDir, "server.csr");
|
|
129
|
+
const extPath = path.join(stateDir, "server-ext.cnf");
|
|
130
|
+
openssl(["ecparam", "-genkey", "-name", "prime256v1", "-noout", "-out", serverKeyPath]);
|
|
131
|
+
openssl(["req", "-new", "-key", serverKeyPath, "-out", csrPath, "-subj", "/CN=localhost"]);
|
|
132
|
+
fs.writeFileSync(
|
|
133
|
+
extPath,
|
|
134
|
+
[
|
|
135
|
+
"authorityKeyIdentifier=keyid,issuer",
|
|
136
|
+
"basicConstraints=CA:FALSE",
|
|
137
|
+
"keyUsage=digitalSignature,keyEncipherment",
|
|
138
|
+
"extendedKeyUsage=serverAuth",
|
|
139
|
+
"subjectAltName=DNS:localhost,DNS:*.localhost"
|
|
140
|
+
].join("\n") + "\n"
|
|
141
|
+
);
|
|
142
|
+
openssl([
|
|
143
|
+
"x509",
|
|
144
|
+
"-req",
|
|
145
|
+
"-in",
|
|
146
|
+
csrPath,
|
|
147
|
+
"-CA",
|
|
148
|
+
caCertPath,
|
|
149
|
+
"-CAkey",
|
|
150
|
+
caKeyPath,
|
|
151
|
+
"-CAcreateserial",
|
|
152
|
+
"-out",
|
|
153
|
+
serverCertPath,
|
|
154
|
+
"-days",
|
|
155
|
+
SERVER_VALIDITY_DAYS.toString(),
|
|
156
|
+
"-extfile",
|
|
157
|
+
extPath
|
|
158
|
+
]);
|
|
159
|
+
for (const tmp of [csrPath, extPath]) {
|
|
160
|
+
try {
|
|
161
|
+
fs.unlinkSync(tmp);
|
|
162
|
+
} catch {
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
fs.chmodSync(serverKeyPath, 384);
|
|
166
|
+
fs.chmodSync(serverCertPath, 420);
|
|
167
|
+
fixOwnership(serverKeyPath, serverCertPath);
|
|
168
|
+
return { certPath: serverCertPath, keyPath: serverKeyPath };
|
|
169
|
+
}
|
|
170
|
+
function ensureCerts(stateDir) {
|
|
171
|
+
const caCertPath = path.join(stateDir, CA_CERT_FILE);
|
|
172
|
+
const caKeyPath = path.join(stateDir, CA_KEY_FILE);
|
|
173
|
+
const serverCertPath = path.join(stateDir, SERVER_CERT_FILE);
|
|
174
|
+
let caGenerated = false;
|
|
175
|
+
if (!fileExists(caCertPath) || !fileExists(caKeyPath) || !isCertValid(caCertPath)) {
|
|
176
|
+
generateCA(stateDir);
|
|
177
|
+
caGenerated = true;
|
|
178
|
+
}
|
|
179
|
+
if (caGenerated || !fileExists(serverCertPath) || !isCertValid(serverCertPath)) {
|
|
180
|
+
generateServerCert(stateDir);
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
certPath: serverCertPath,
|
|
184
|
+
keyPath: path.join(stateDir, SERVER_KEY_FILE),
|
|
185
|
+
caPath: caCertPath,
|
|
186
|
+
caGenerated
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function isCATrusted(stateDir) {
|
|
190
|
+
const caCertPath = path.join(stateDir, CA_CERT_FILE);
|
|
191
|
+
if (!fileExists(caCertPath)) return false;
|
|
192
|
+
if (process.platform === "darwin") {
|
|
193
|
+
return isCATrustedMacOS(caCertPath);
|
|
194
|
+
} else if (process.platform === "linux") {
|
|
195
|
+
return isCATrustedLinux(stateDir);
|
|
196
|
+
}
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
function isCATrustedMacOS(caCertPath) {
|
|
200
|
+
try {
|
|
201
|
+
const fingerprint = openssl(["x509", "-in", caCertPath, "-noout", "-fingerprint", "-sha1"]).trim().replace(/^.*=/, "").replace(/:/g, "").toLowerCase();
|
|
202
|
+
for (const keychain of [loginKeychainPath(), "/Library/Keychains/System.keychain"]) {
|
|
203
|
+
try {
|
|
204
|
+
const result = execFileSync("security", ["find-certificate", "-a", "-Z", keychain], {
|
|
205
|
+
encoding: "utf-8",
|
|
206
|
+
timeout: 5e3,
|
|
207
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
208
|
+
});
|
|
209
|
+
if (result.toLowerCase().includes(fingerprint)) return true;
|
|
210
|
+
} catch {
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return false;
|
|
214
|
+
} catch {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function loginKeychainPath() {
|
|
219
|
+
try {
|
|
220
|
+
const result = execFileSync("security", ["default-keychain"], {
|
|
221
|
+
encoding: "utf-8",
|
|
222
|
+
timeout: 5e3
|
|
223
|
+
}).trim();
|
|
224
|
+
const match = result.match(/"(.+)"/);
|
|
225
|
+
if (match) return match[1];
|
|
226
|
+
} catch {
|
|
227
|
+
}
|
|
228
|
+
const home = process.env.HOME || `/Users/${process.env.USER || "unknown"}`;
|
|
229
|
+
return path.join(home, "Library", "Keychains", "login.keychain-db");
|
|
230
|
+
}
|
|
231
|
+
function isCATrustedLinux(stateDir) {
|
|
232
|
+
const systemCertPath = `/usr/local/share/ca-certificates/portless-ca.crt`;
|
|
233
|
+
if (!fileExists(systemCertPath)) return false;
|
|
234
|
+
try {
|
|
235
|
+
const ours = fs.readFileSync(path.join(stateDir, CA_CERT_FILE), "utf-8").trim();
|
|
236
|
+
const installed = fs.readFileSync(systemCertPath, "utf-8").trim();
|
|
237
|
+
return ours === installed;
|
|
238
|
+
} catch {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
var HOST_CERTS_DIR = "host-certs";
|
|
243
|
+
function sanitizeHostForFilename(hostname) {
|
|
244
|
+
return hostname.replace(/\./g, "_").replace(/[^a-z0-9_-]/gi, "");
|
|
245
|
+
}
|
|
246
|
+
async function generateHostCertAsync(stateDir, hostname) {
|
|
247
|
+
const caKeyPath = path.join(stateDir, CA_KEY_FILE);
|
|
248
|
+
const caCertPath = path.join(stateDir, CA_CERT_FILE);
|
|
249
|
+
const hostDir = path.join(stateDir, HOST_CERTS_DIR);
|
|
250
|
+
if (!fs.existsSync(hostDir)) {
|
|
251
|
+
await fs.promises.mkdir(hostDir, { recursive: true, mode: 493 });
|
|
252
|
+
fixOwnership(hostDir);
|
|
253
|
+
}
|
|
254
|
+
const safeName = sanitizeHostForFilename(hostname);
|
|
255
|
+
const keyPath = path.join(hostDir, `${safeName}-key.pem`);
|
|
256
|
+
const certPath = path.join(hostDir, `${safeName}.pem`);
|
|
257
|
+
const csrPath = path.join(hostDir, `${safeName}.csr`);
|
|
258
|
+
const extPath = path.join(hostDir, `${safeName}-ext.cnf`);
|
|
259
|
+
await opensslAsync(["ecparam", "-genkey", "-name", "prime256v1", "-noout", "-out", keyPath]);
|
|
260
|
+
await opensslAsync(["req", "-new", "-key", keyPath, "-out", csrPath, "-subj", `/CN=${hostname}`]);
|
|
261
|
+
const sans = [`DNS:${hostname}`];
|
|
262
|
+
const parts = hostname.split(".");
|
|
263
|
+
if (parts.length >= 2) {
|
|
264
|
+
sans.push(`DNS:*.${parts.slice(1).join(".")}`);
|
|
265
|
+
}
|
|
266
|
+
await fs.promises.writeFile(
|
|
267
|
+
extPath,
|
|
268
|
+
[
|
|
269
|
+
"authorityKeyIdentifier=keyid,issuer",
|
|
270
|
+
"basicConstraints=CA:FALSE",
|
|
271
|
+
"keyUsage=digitalSignature,keyEncipherment",
|
|
272
|
+
"extendedKeyUsage=serverAuth",
|
|
273
|
+
`subjectAltName=${sans.join(",")}`
|
|
274
|
+
].join("\n") + "\n"
|
|
275
|
+
);
|
|
276
|
+
await opensslAsync([
|
|
277
|
+
"x509",
|
|
278
|
+
"-req",
|
|
279
|
+
"-in",
|
|
280
|
+
csrPath,
|
|
281
|
+
"-CA",
|
|
282
|
+
caCertPath,
|
|
283
|
+
"-CAkey",
|
|
284
|
+
caKeyPath,
|
|
285
|
+
"-CAcreateserial",
|
|
286
|
+
"-out",
|
|
287
|
+
certPath,
|
|
288
|
+
"-days",
|
|
289
|
+
SERVER_VALIDITY_DAYS.toString(),
|
|
290
|
+
"-extfile",
|
|
291
|
+
extPath
|
|
292
|
+
]);
|
|
293
|
+
for (const tmp of [csrPath, extPath]) {
|
|
294
|
+
try {
|
|
295
|
+
await fs.promises.unlink(tmp);
|
|
296
|
+
} catch {
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
await fs.promises.chmod(keyPath, 384);
|
|
300
|
+
await fs.promises.chmod(certPath, 420);
|
|
301
|
+
fixOwnership(keyPath, certPath);
|
|
302
|
+
return { certPath, keyPath };
|
|
303
|
+
}
|
|
304
|
+
function isSimpleLocalhostSubdomain(hostname) {
|
|
305
|
+
const parts = hostname.split(".");
|
|
306
|
+
return parts.length === 2 && parts[1] === "localhost";
|
|
307
|
+
}
|
|
308
|
+
function createSNICallback(stateDir, defaultCert, defaultKey) {
|
|
309
|
+
const cache = /* @__PURE__ */ new Map();
|
|
310
|
+
const pending = /* @__PURE__ */ new Map();
|
|
311
|
+
const defaultCtx = tls.createSecureContext({ cert: defaultCert, key: defaultKey });
|
|
312
|
+
return (servername, cb) => {
|
|
313
|
+
if (servername === "localhost" || isSimpleLocalhostSubdomain(servername)) {
|
|
314
|
+
cb(null, defaultCtx);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (cache.has(servername)) {
|
|
318
|
+
cb(null, cache.get(servername));
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const safeName = sanitizeHostForFilename(servername);
|
|
322
|
+
const hostDir = path.join(stateDir, HOST_CERTS_DIR);
|
|
323
|
+
const certPath = path.join(hostDir, `${safeName}.pem`);
|
|
324
|
+
const keyPath = path.join(hostDir, `${safeName}-key.pem`);
|
|
325
|
+
if (fileExists(certPath) && fileExists(keyPath) && isCertValid(certPath)) {
|
|
326
|
+
try {
|
|
327
|
+
const ctx = tls.createSecureContext({
|
|
328
|
+
cert: fs.readFileSync(certPath),
|
|
329
|
+
key: fs.readFileSync(keyPath)
|
|
330
|
+
});
|
|
331
|
+
cache.set(servername, ctx);
|
|
332
|
+
cb(null, ctx);
|
|
333
|
+
return;
|
|
334
|
+
} catch {
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (pending.has(servername)) {
|
|
338
|
+
pending.get(servername).then((ctx) => cb(null, ctx)).catch((err) => cb(err instanceof Error ? err : new Error(String(err))));
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const promise = generateHostCertAsync(stateDir, servername).then(async (generated) => {
|
|
342
|
+
const [cert, key] = await Promise.all([
|
|
343
|
+
fs.promises.readFile(generated.certPath),
|
|
344
|
+
fs.promises.readFile(generated.keyPath)
|
|
345
|
+
]);
|
|
346
|
+
return tls.createSecureContext({ cert, key });
|
|
347
|
+
});
|
|
348
|
+
pending.set(servername, promise);
|
|
349
|
+
promise.then((ctx) => {
|
|
350
|
+
cache.set(servername, ctx);
|
|
351
|
+
pending.delete(servername);
|
|
352
|
+
cb(null, ctx);
|
|
353
|
+
}).catch((err) => {
|
|
354
|
+
pending.delete(servername);
|
|
355
|
+
cb(err instanceof Error ? err : new Error(String(err)));
|
|
356
|
+
});
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
function trustCA(stateDir) {
|
|
360
|
+
const caCertPath = path.join(stateDir, CA_CERT_FILE);
|
|
361
|
+
if (!fileExists(caCertPath)) {
|
|
362
|
+
return { trusted: false, error: "CA certificate not found. Run with --https first." };
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
if (process.platform === "darwin") {
|
|
366
|
+
const keychain = loginKeychainPath();
|
|
367
|
+
execFileSync(
|
|
368
|
+
"security",
|
|
369
|
+
["add-trusted-cert", "-r", "trustRoot", "-k", keychain, caCertPath],
|
|
370
|
+
{ stdio: "pipe", timeout: 3e4 }
|
|
371
|
+
);
|
|
372
|
+
return { trusted: true };
|
|
373
|
+
} else if (process.platform === "linux") {
|
|
374
|
+
const dest = "/usr/local/share/ca-certificates/portless-ca.crt";
|
|
375
|
+
fs.copyFileSync(caCertPath, dest);
|
|
376
|
+
execFileSync("update-ca-certificates", [], { stdio: "pipe", timeout: 3e4 });
|
|
377
|
+
return { trusted: true };
|
|
378
|
+
}
|
|
379
|
+
return { trusted: false, error: `Unsupported platform: ${process.platform}` };
|
|
380
|
+
} catch (err) {
|
|
381
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
382
|
+
if (message.includes("authorization") || message.includes("permission") || message.includes("EACCES")) {
|
|
383
|
+
return {
|
|
384
|
+
trusted: false,
|
|
385
|
+
error: "Permission denied. Try: sudo portless trust"
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
return { trusted: false, error: message };
|
|
389
|
+
}
|
|
390
|
+
}
|
|
15
391
|
|
|
16
392
|
// src/cli-utils.ts
|
|
393
|
+
import * as fs2 from "fs";
|
|
394
|
+
import * as http from "http";
|
|
395
|
+
import * as https from "https";
|
|
17
396
|
import * as net from "net";
|
|
18
|
-
|
|
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
|
+
}
|
|
19
492
|
const tryPort = (port) => {
|
|
20
493
|
return new Promise((resolve) => {
|
|
21
494
|
const server = net.createServer();
|
|
@@ -25,7 +498,7 @@ async function findFreePort(minPort = 4e3, maxPort = 4999) {
|
|
|
25
498
|
server.on("error", () => resolve(false));
|
|
26
499
|
});
|
|
27
500
|
};
|
|
28
|
-
for (let i = 0; i <
|
|
501
|
+
for (let i = 0; i < RANDOM_PORT_ATTEMPTS; i++) {
|
|
29
502
|
const port = minPort + Math.floor(Math.random() * (maxPort - minPort + 1));
|
|
30
503
|
if (await tryPort(port)) {
|
|
31
504
|
return port;
|
|
@@ -38,58 +511,36 @@ async function findFreePort(minPort = 4e3, maxPort = 4999) {
|
|
|
38
511
|
}
|
|
39
512
|
throw new Error(`No free port found in range ${minPort}-${maxPort}`);
|
|
40
513
|
}
|
|
41
|
-
function isProxyRunning(port =
|
|
514
|
+
function isProxyRunning(port, tls2 = false) {
|
|
42
515
|
return new Promise((resolve) => {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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();
|
|
52
534
|
resolve(false);
|
|
53
535
|
});
|
|
54
|
-
|
|
536
|
+
req.end();
|
|
55
537
|
});
|
|
56
538
|
}
|
|
57
|
-
|
|
58
|
-
// src/cli.ts
|
|
59
|
-
var SIGNAL_CODES = { SIGINT: 2, SIGTERM: 15 };
|
|
60
|
-
function prompt(question) {
|
|
61
|
-
const rl = readline.createInterface({
|
|
62
|
-
input: process.stdin,
|
|
63
|
-
output: process.stdout
|
|
64
|
-
});
|
|
65
|
-
return new Promise((resolve) => {
|
|
66
|
-
rl.on("close", () => resolve(""));
|
|
67
|
-
rl.question(question, (answer) => {
|
|
68
|
-
rl.close();
|
|
69
|
-
resolve(answer.trim().toLowerCase());
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
var PORTLESS_DIR = "/tmp/portless";
|
|
74
|
-
var PROXY_PORT_PATH = path.join(PORTLESS_DIR, "proxy.port");
|
|
75
|
-
var DEFAULT_PROXY_PORT = 80;
|
|
76
|
-
var store = new RouteStore(PORTLESS_DIR, {
|
|
77
|
-
onWarning: (msg) => console.warn(chalk.yellow(msg))
|
|
78
|
-
});
|
|
79
|
-
function readProxyPort() {
|
|
80
|
-
try {
|
|
81
|
-
const raw = fs.readFileSync(PROXY_PORT_PATH, "utf-8").trim();
|
|
82
|
-
const port = parseInt(raw, 10);
|
|
83
|
-
return isNaN(port) ? DEFAULT_PROXY_PORT : port;
|
|
84
|
-
} catch {
|
|
85
|
-
return DEFAULT_PROXY_PORT;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
539
|
function findPidOnPort(port) {
|
|
89
540
|
try {
|
|
90
541
|
const output = execSync(`lsof -ti tcp:${port} -sTCP:LISTEN`, {
|
|
91
542
|
encoding: "utf-8",
|
|
92
|
-
timeout:
|
|
543
|
+
timeout: LSOF_TIMEOUT_MS
|
|
93
544
|
});
|
|
94
545
|
const pid = parseInt(output.trim().split("\n")[0], 10);
|
|
95
546
|
return isNaN(pid) ? null : pid;
|
|
@@ -97,11 +548,10 @@ function findPidOnPort(port) {
|
|
|
97
548
|
return null;
|
|
98
549
|
}
|
|
99
550
|
}
|
|
100
|
-
async function waitForProxy(
|
|
101
|
-
const port = proxyPort ?? readProxyPort();
|
|
551
|
+
async function waitForProxy(port, maxAttempts = WAIT_FOR_PROXY_MAX_ATTEMPTS, intervalMs = WAIT_FOR_PROXY_INTERVAL_MS, tls2 = false) {
|
|
102
552
|
for (let i = 0; i < maxAttempts; i++) {
|
|
103
553
|
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
104
|
-
if (await isProxyRunning(port)) {
|
|
554
|
+
if (await isProxyRunning(port, tls2)) {
|
|
105
555
|
return true;
|
|
106
556
|
}
|
|
107
557
|
}
|
|
@@ -113,45 +563,76 @@ function spawnCommand(commandArgs, options) {
|
|
|
113
563
|
env: options?.env
|
|
114
564
|
});
|
|
115
565
|
let exiting = false;
|
|
566
|
+
const cleanup = () => {
|
|
567
|
+
process.removeListener("SIGINT", onSigInt);
|
|
568
|
+
process.removeListener("SIGTERM", onSigTerm);
|
|
569
|
+
options?.onCleanup?.();
|
|
570
|
+
};
|
|
116
571
|
const handleSignal = (signal) => {
|
|
117
572
|
if (exiting) return;
|
|
118
573
|
exiting = true;
|
|
119
574
|
child.kill(signal);
|
|
120
|
-
|
|
575
|
+
cleanup();
|
|
121
576
|
process.exit(128 + (SIGNAL_CODES[signal] || 15));
|
|
122
577
|
};
|
|
123
|
-
|
|
124
|
-
|
|
578
|
+
const onSigInt = () => handleSignal("SIGINT");
|
|
579
|
+
const onSigTerm = () => handleSignal("SIGTERM");
|
|
580
|
+
process.on("SIGINT", onSigInt);
|
|
581
|
+
process.on("SIGTERM", onSigTerm);
|
|
125
582
|
child.on("error", (err) => {
|
|
126
583
|
if (exiting) return;
|
|
127
584
|
exiting = true;
|
|
128
|
-
console.error(
|
|
129
|
-
|
|
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();
|
|
130
590
|
process.exit(1);
|
|
131
591
|
});
|
|
132
592
|
child.on("exit", (code, signal) => {
|
|
133
593
|
if (exiting) return;
|
|
134
594
|
exiting = true;
|
|
135
|
-
|
|
595
|
+
cleanup();
|
|
136
596
|
if (signal) {
|
|
137
|
-
process.exit(128 + (SIGNAL_CODES[signal] ||
|
|
597
|
+
process.exit(128 + (SIGNAL_CODES[signal] || 15));
|
|
138
598
|
}
|
|
139
599
|
process.exit(code ?? 1);
|
|
140
600
|
});
|
|
141
601
|
}
|
|
142
|
-
function
|
|
602
|
+
function prompt(question) {
|
|
603
|
+
const rl = readline.createInterface({
|
|
604
|
+
input: process.stdin,
|
|
605
|
+
output: process.stdout
|
|
606
|
+
});
|
|
607
|
+
return new Promise((resolve) => {
|
|
608
|
+
rl.on("close", () => resolve(""));
|
|
609
|
+
rl.question(question, (answer) => {
|
|
610
|
+
rl.close();
|
|
611
|
+
resolve(answer.trim().toLowerCase());
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// src/cli.ts
|
|
617
|
+
var DEBOUNCE_MS = 100;
|
|
618
|
+
var POLL_INTERVAL_MS = 3e3;
|
|
619
|
+
var EXIT_TIMEOUT_MS = 2e3;
|
|
620
|
+
var SUDO_SPAWN_TIMEOUT_MS = 3e4;
|
|
621
|
+
function startProxyServer(store, proxyPort, tlsOptions) {
|
|
143
622
|
store.ensureDir();
|
|
623
|
+
const isTls = !!tlsOptions;
|
|
144
624
|
const routesPath = store.getRoutesPath();
|
|
145
|
-
if (!
|
|
146
|
-
|
|
625
|
+
if (!fs3.existsSync(routesPath)) {
|
|
626
|
+
fs3.writeFileSync(routesPath, "[]", { mode: FILE_MODE });
|
|
147
627
|
}
|
|
148
628
|
try {
|
|
149
|
-
|
|
629
|
+
fs3.chmodSync(routesPath, FILE_MODE);
|
|
150
630
|
} catch {
|
|
151
631
|
}
|
|
152
632
|
let cachedRoutes = store.loadRoutes();
|
|
153
633
|
let debounceTimer = null;
|
|
154
634
|
let watcher = null;
|
|
635
|
+
let pollingInterval = null;
|
|
155
636
|
const reloadRoutes = () => {
|
|
156
637
|
try {
|
|
157
638
|
cachedRoutes = store.loadRoutes();
|
|
@@ -159,86 +640,107 @@ function startProxyServer(proxyPort) {
|
|
|
159
640
|
}
|
|
160
641
|
};
|
|
161
642
|
try {
|
|
162
|
-
watcher =
|
|
643
|
+
watcher = fs3.watch(routesPath, () => {
|
|
163
644
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
164
|
-
debounceTimer = setTimeout(reloadRoutes,
|
|
645
|
+
debounceTimer = setTimeout(reloadRoutes, DEBOUNCE_MS);
|
|
165
646
|
});
|
|
166
647
|
} catch {
|
|
167
648
|
console.warn(chalk.yellow("fs.watch unavailable; falling back to polling for route changes"));
|
|
168
|
-
setInterval(reloadRoutes,
|
|
649
|
+
pollingInterval = setInterval(reloadRoutes, POLL_INTERVAL_MS);
|
|
169
650
|
}
|
|
170
651
|
const server = createProxyServer({
|
|
171
652
|
getRoutes: () => cachedRoutes,
|
|
172
|
-
|
|
653
|
+
proxyPort,
|
|
654
|
+
onError: (msg) => console.error(chalk.red(msg)),
|
|
655
|
+
tls: tlsOptions
|
|
173
656
|
});
|
|
174
657
|
server.on("error", (err) => {
|
|
175
658
|
if (err.code === "EADDRINUSE") {
|
|
176
|
-
console.error(chalk.red(`Port ${proxyPort} is already in use
|
|
659
|
+
console.error(chalk.red(`Port ${proxyPort} is already in use.`));
|
|
660
|
+
console.error(chalk.blue("Stop the existing proxy first:"));
|
|
661
|
+
console.error(chalk.cyan(" portless proxy stop"));
|
|
662
|
+
console.error(chalk.blue("Or check what is using the port:"));
|
|
663
|
+
console.error(chalk.cyan(` lsof -ti tcp:${proxyPort}`));
|
|
177
664
|
} else if (err.code === "EACCES") {
|
|
178
|
-
console.error(chalk.red(
|
|
665
|
+
console.error(chalk.red(`Permission denied for port ${proxyPort}.`));
|
|
666
|
+
console.error(chalk.blue("Either run with sudo:"));
|
|
667
|
+
console.error(chalk.cyan(" sudo portless proxy start -p 80"));
|
|
668
|
+
console.error(chalk.blue("Or use a non-privileged port (no sudo needed):"));
|
|
669
|
+
console.error(chalk.cyan(" portless proxy start"));
|
|
179
670
|
} else {
|
|
180
671
|
console.error(chalk.red(`Proxy error: ${err.message}`));
|
|
181
672
|
}
|
|
182
673
|
process.exit(1);
|
|
183
674
|
});
|
|
184
675
|
server.listen(proxyPort, () => {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
676
|
+
fs3.writeFileSync(store.pidPath, process.pid.toString(), { mode: FILE_MODE });
|
|
677
|
+
fs3.writeFileSync(store.portFilePath, proxyPort.toString(), { mode: FILE_MODE });
|
|
678
|
+
writeTlsMarker(store.dir, isTls);
|
|
679
|
+
const proto = isTls ? "HTTPS/2" : "HTTP";
|
|
680
|
+
console.log(chalk.green(`${proto} proxy listening on port ${proxyPort}`));
|
|
188
681
|
});
|
|
189
682
|
let exiting = false;
|
|
190
683
|
const cleanup = () => {
|
|
191
684
|
if (exiting) return;
|
|
192
685
|
exiting = true;
|
|
686
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
687
|
+
if (pollingInterval) clearInterval(pollingInterval);
|
|
193
688
|
if (watcher) {
|
|
194
689
|
watcher.close();
|
|
195
690
|
}
|
|
196
691
|
try {
|
|
197
|
-
|
|
692
|
+
fs3.unlinkSync(store.pidPath);
|
|
198
693
|
} catch {
|
|
199
694
|
}
|
|
200
695
|
try {
|
|
201
|
-
|
|
696
|
+
fs3.unlinkSync(store.portFilePath);
|
|
202
697
|
} catch {
|
|
203
698
|
}
|
|
699
|
+
writeTlsMarker(store.dir, false);
|
|
204
700
|
server.close(() => process.exit(0));
|
|
205
|
-
setTimeout(() => process.exit(0),
|
|
701
|
+
setTimeout(() => process.exit(0), EXIT_TIMEOUT_MS).unref();
|
|
206
702
|
};
|
|
207
703
|
process.on("SIGINT", cleanup);
|
|
208
704
|
process.on("SIGTERM", cleanup);
|
|
209
705
|
console.log(chalk.cyan("\nProxy is running. Press Ctrl+C to stop.\n"));
|
|
210
706
|
console.log(chalk.gray(`Routes file: ${store.getRoutesPath()}`));
|
|
211
707
|
}
|
|
212
|
-
async function stopProxy() {
|
|
708
|
+
async function stopProxy(store, proxyPort, tls2) {
|
|
213
709
|
const pidPath = store.pidPath;
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
710
|
+
const needsSudo = proxyPort < PRIVILEGED_PORT_THRESHOLD;
|
|
711
|
+
const sudoHint = needsSudo ? "sudo " : "";
|
|
712
|
+
if (!fs3.existsSync(pidPath)) {
|
|
713
|
+
if (await isProxyRunning(proxyPort, tls2)) {
|
|
217
714
|
console.log(chalk.yellow(`PID file is missing but port ${proxyPort} is still in use.`));
|
|
218
715
|
const pid = findPidOnPort(proxyPort);
|
|
219
716
|
if (pid !== null) {
|
|
220
717
|
try {
|
|
221
718
|
process.kill(pid, "SIGTERM");
|
|
222
719
|
try {
|
|
223
|
-
|
|
720
|
+
fs3.unlinkSync(store.portFilePath);
|
|
224
721
|
} catch {
|
|
225
722
|
}
|
|
226
723
|
console.log(chalk.green(`Killed process ${pid}. Proxy stopped.`));
|
|
227
724
|
} catch (err) {
|
|
228
725
|
if (isErrnoException(err) && err.code === "EPERM") {
|
|
229
|
-
console.error(chalk.red("Permission denied. The proxy
|
|
230
|
-
console.
|
|
726
|
+
console.error(chalk.red("Permission denied. The proxy was started with sudo."));
|
|
727
|
+
console.error(chalk.blue("Stop it with:"));
|
|
728
|
+
console.error(chalk.cyan(" sudo portless proxy stop"));
|
|
231
729
|
} else {
|
|
232
730
|
const message = err instanceof Error ? err.message : String(err);
|
|
233
|
-
console.error(chalk.red(
|
|
731
|
+
console.error(chalk.red(`Failed to stop proxy: ${message}`));
|
|
732
|
+
console.error(chalk.blue("Check if the process is still running:"));
|
|
733
|
+
console.error(chalk.cyan(` lsof -ti tcp:${proxyPort}`));
|
|
234
734
|
}
|
|
235
735
|
}
|
|
236
736
|
} else if (process.getuid?.() !== 0) {
|
|
237
|
-
console.error(chalk.red("
|
|
238
|
-
console.
|
|
737
|
+
console.error(chalk.red("Cannot identify the process. It may be running as root."));
|
|
738
|
+
console.error(chalk.blue("Try stopping with sudo:"));
|
|
739
|
+
console.error(chalk.cyan(" sudo portless proxy stop"));
|
|
239
740
|
} else {
|
|
240
741
|
console.error(chalk.red(`Could not identify the process on port ${proxyPort}.`));
|
|
241
|
-
console.
|
|
742
|
+
console.error(chalk.blue("Try manually:"));
|
|
743
|
+
console.error(chalk.cyan(` sudo kill "$(lsof -ti tcp:${proxyPort})"`));
|
|
242
744
|
}
|
|
243
745
|
} else {
|
|
244
746
|
console.log(chalk.yellow("Proxy is not running."));
|
|
@@ -246,47 +748,54 @@ async function stopProxy() {
|
|
|
246
748
|
return;
|
|
247
749
|
}
|
|
248
750
|
try {
|
|
249
|
-
const pid = parseInt(
|
|
751
|
+
const pid = parseInt(fs3.readFileSync(pidPath, "utf-8"), 10);
|
|
250
752
|
if (isNaN(pid)) {
|
|
251
753
|
console.error(chalk.red("Corrupted PID file. Removing it."));
|
|
252
|
-
|
|
754
|
+
fs3.unlinkSync(pidPath);
|
|
253
755
|
return;
|
|
254
756
|
}
|
|
255
757
|
try {
|
|
256
758
|
process.kill(pid, 0);
|
|
257
759
|
} catch {
|
|
258
|
-
console.log(chalk.yellow("Proxy process is no longer running. Cleaning up."));
|
|
259
|
-
|
|
760
|
+
console.log(chalk.yellow("Proxy process is no longer running. Cleaning up stale files."));
|
|
761
|
+
fs3.unlinkSync(pidPath);
|
|
762
|
+
try {
|
|
763
|
+
fs3.unlinkSync(store.portFilePath);
|
|
764
|
+
} catch {
|
|
765
|
+
}
|
|
260
766
|
return;
|
|
261
767
|
}
|
|
262
|
-
if (!await isProxyRunning(proxyPort)) {
|
|
768
|
+
if (!await isProxyRunning(proxyPort, tls2)) {
|
|
263
769
|
console.log(
|
|
264
770
|
chalk.yellow(
|
|
265
771
|
`PID file exists but port ${proxyPort} is not listening. The PID may have been recycled.`
|
|
266
772
|
)
|
|
267
773
|
);
|
|
268
774
|
console.log(chalk.yellow("Removing stale PID file."));
|
|
269
|
-
|
|
775
|
+
fs3.unlinkSync(pidPath);
|
|
270
776
|
return;
|
|
271
777
|
}
|
|
272
778
|
process.kill(pid, "SIGTERM");
|
|
273
|
-
|
|
779
|
+
fs3.unlinkSync(pidPath);
|
|
274
780
|
try {
|
|
275
|
-
|
|
781
|
+
fs3.unlinkSync(store.portFilePath);
|
|
276
782
|
} catch {
|
|
277
783
|
}
|
|
278
784
|
console.log(chalk.green("Proxy stopped."));
|
|
279
785
|
} catch (err) {
|
|
280
786
|
if (isErrnoException(err) && err.code === "EPERM") {
|
|
281
|
-
console.error(chalk.red("Permission denied. The proxy
|
|
282
|
-
console.
|
|
787
|
+
console.error(chalk.red("Permission denied. The proxy was started with sudo."));
|
|
788
|
+
console.error(chalk.blue("Stop it with:"));
|
|
789
|
+
console.error(chalk.cyan(` ${sudoHint}portless proxy stop`));
|
|
283
790
|
} else {
|
|
284
791
|
const message = err instanceof Error ? err.message : String(err);
|
|
285
|
-
console.error(chalk.red(
|
|
792
|
+
console.error(chalk.red(`Failed to stop proxy: ${message}`));
|
|
793
|
+
console.error(chalk.blue("Check if the process is still running:"));
|
|
794
|
+
console.error(chalk.cyan(` lsof -ti tcp:${proxyPort}`));
|
|
286
795
|
}
|
|
287
796
|
}
|
|
288
797
|
}
|
|
289
|
-
function listRoutes() {
|
|
798
|
+
function listRoutes(store, proxyPort, tls2) {
|
|
290
799
|
const routes = store.loadRoutes();
|
|
291
800
|
if (routes.length === 0) {
|
|
292
801
|
console.log(chalk.yellow("No active routes."));
|
|
@@ -295,21 +804,32 @@ function listRoutes() {
|
|
|
295
804
|
}
|
|
296
805
|
console.log(chalk.blue.bold("\nActive routes:\n"));
|
|
297
806
|
for (const route of routes) {
|
|
807
|
+
const url = formatUrl(route.hostname, proxyPort, tls2);
|
|
298
808
|
console.log(
|
|
299
|
-
` ${chalk.cyan(
|
|
809
|
+
` ${chalk.cyan(url)} ${chalk.gray("->")} ${chalk.white(`localhost:${route.port}`)} ${chalk.gray(`(pid ${route.pid})`)}`
|
|
300
810
|
);
|
|
301
811
|
}
|
|
302
812
|
console.log();
|
|
303
813
|
}
|
|
304
|
-
async function runApp(name, commandArgs) {
|
|
814
|
+
async function runApp(store, proxyPort, stateDir, name, commandArgs, tls2) {
|
|
305
815
|
const hostname = parseHostname(name);
|
|
306
|
-
const proxyPort = readProxyPort();
|
|
307
816
|
console.log(chalk.blue.bold(`
|
|
308
817
|
portless
|
|
309
818
|
`));
|
|
310
819
|
console.log(chalk.gray(`-- ${hostname} (auto-resolves to 127.0.0.1)`));
|
|
311
|
-
if (!await isProxyRunning(proxyPort)) {
|
|
312
|
-
|
|
820
|
+
if (!await isProxyRunning(proxyPort, tls2)) {
|
|
821
|
+
const defaultPort = getDefaultPort();
|
|
822
|
+
const needsSudo = defaultPort < PRIVILEGED_PORT_THRESHOLD;
|
|
823
|
+
const wantHttps = isHttpsEnvEnabled();
|
|
824
|
+
if (needsSudo) {
|
|
825
|
+
if (!process.stdin.isTTY) {
|
|
826
|
+
console.error(chalk.red("Proxy is not running."));
|
|
827
|
+
console.error(chalk.blue("Start the proxy first (requires sudo for this port):"));
|
|
828
|
+
console.error(chalk.cyan(" sudo portless proxy start -p 80"));
|
|
829
|
+
console.error(chalk.blue("Or use the default port (no sudo needed):"));
|
|
830
|
+
console.error(chalk.cyan(" portless proxy start"));
|
|
831
|
+
process.exit(1);
|
|
832
|
+
}
|
|
313
833
|
const answer = await prompt(chalk.yellow("Proxy not running. Start it? [Y/n/skip] "));
|
|
314
834
|
if (answer === "n" || answer === "no") {
|
|
315
835
|
console.log(chalk.gray("Cancelled."));
|
|
@@ -321,37 +841,55 @@ portless
|
|
|
321
841
|
return;
|
|
322
842
|
}
|
|
323
843
|
console.log(chalk.yellow("Starting proxy (requires sudo)..."));
|
|
324
|
-
const
|
|
844
|
+
const startArgs = [process.execPath, process.argv[1], "proxy", "start"];
|
|
845
|
+
if (wantHttps) startArgs.push("--https");
|
|
846
|
+
const result = spawnSync("sudo", startArgs, {
|
|
325
847
|
stdio: "inherit",
|
|
326
|
-
timeout:
|
|
848
|
+
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
327
849
|
});
|
|
328
850
|
if (result.status !== 0) {
|
|
329
|
-
console.
|
|
851
|
+
console.error(chalk.red("Failed to start proxy."));
|
|
852
|
+
console.error(chalk.blue("Try starting it manually:"));
|
|
853
|
+
console.error(chalk.cyan(" sudo portless proxy start"));
|
|
330
854
|
process.exit(1);
|
|
331
855
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
856
|
+
} else {
|
|
857
|
+
console.log(chalk.yellow("Starting proxy..."));
|
|
858
|
+
const startArgs = [process.argv[1], "proxy", "start"];
|
|
859
|
+
if (wantHttps) startArgs.push("--https");
|
|
860
|
+
const result = spawnSync(process.execPath, startArgs, {
|
|
861
|
+
stdio: "inherit",
|
|
862
|
+
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
863
|
+
});
|
|
864
|
+
if (result.status !== 0) {
|
|
865
|
+
console.error(chalk.red("Failed to start proxy."));
|
|
866
|
+
console.error(chalk.blue("Try starting it manually:"));
|
|
867
|
+
console.error(chalk.cyan(" portless proxy start"));
|
|
338
868
|
process.exit(1);
|
|
339
869
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
console.
|
|
344
|
-
|
|
870
|
+
}
|
|
871
|
+
const autoTls = readTlsMarker(stateDir);
|
|
872
|
+
if (!await waitForProxy(defaultPort, void 0, void 0, autoTls)) {
|
|
873
|
+
console.error(chalk.red("Proxy failed to start (timed out waiting for it to listen)."));
|
|
874
|
+
const logPath = path3.join(stateDir, "proxy.log");
|
|
875
|
+
console.error(chalk.blue("Try starting the proxy manually to see the error:"));
|
|
876
|
+
console.error(chalk.cyan(` ${needsSudo ? "sudo " : ""}portless proxy start`));
|
|
877
|
+
if (fs3.existsSync(logPath)) {
|
|
878
|
+
console.error(chalk.gray(`Logs: ${logPath}`));
|
|
879
|
+
}
|
|
345
880
|
process.exit(1);
|
|
346
881
|
}
|
|
882
|
+
tls2 = autoTls;
|
|
883
|
+
console.log(chalk.green("Proxy started in background"));
|
|
347
884
|
} else {
|
|
348
885
|
console.log(chalk.gray("-- Proxy is running"));
|
|
349
886
|
}
|
|
350
887
|
const port = await findFreePort();
|
|
351
888
|
console.log(chalk.green(`-- Using port ${port}`));
|
|
352
889
|
store.addRoute(hostname, port, process.pid);
|
|
890
|
+
const finalUrl = formatUrl(hostname, proxyPort, tls2);
|
|
353
891
|
console.log(chalk.cyan.bold(`
|
|
354
|
-
->
|
|
892
|
+
-> ${finalUrl}
|
|
355
893
|
`));
|
|
356
894
|
console.log(chalk.gray(`Running: PORT=${port} ${commandArgs.join(" ")}
|
|
357
895
|
`));
|
|
@@ -371,7 +909,8 @@ async function main() {
|
|
|
371
909
|
const isPnpmDlx = !!process.env.PNPM_SCRIPT_SRC_DIR && !process.env.npm_lifecycle_event;
|
|
372
910
|
if (isNpx || isPnpmDlx) {
|
|
373
911
|
console.error(chalk.red("Error: portless should not be run via npx or pnpm dlx."));
|
|
374
|
-
console.
|
|
912
|
+
console.error(chalk.blue("Install globally instead:"));
|
|
913
|
+
console.error(chalk.cyan(" npm install -g portless"));
|
|
375
914
|
process.exit(1);
|
|
376
915
|
}
|
|
377
916
|
const skipPortless = process.env.PORTLESS === "0" || process.env.PORTLESS === "skip";
|
|
@@ -391,16 +930,19 @@ ${chalk.bold("Install:")}
|
|
|
391
930
|
Do NOT add portless as a project dependency.
|
|
392
931
|
|
|
393
932
|
${chalk.bold("Usage:")}
|
|
394
|
-
${chalk.cyan("
|
|
395
|
-
${chalk.cyan("
|
|
396
|
-
${chalk.cyan("
|
|
933
|
+
${chalk.cyan("portless proxy start")} Start the proxy (background daemon)
|
|
934
|
+
${chalk.cyan("portless proxy start --https")} Start with HTTP/2 + TLS (auto-generates certs)
|
|
935
|
+
${chalk.cyan("portless proxy start -p 80")} Start on port 80 (requires sudo)
|
|
936
|
+
${chalk.cyan("portless proxy stop")} Stop the proxy
|
|
397
937
|
${chalk.cyan("portless <name> <cmd>")} Run your app through the proxy
|
|
398
938
|
${chalk.cyan("portless list")} Show active routes
|
|
939
|
+
${chalk.cyan("portless trust")} Add local CA to system trust store
|
|
399
940
|
|
|
400
941
|
${chalk.bold("Examples:")}
|
|
401
|
-
|
|
402
|
-
portless
|
|
403
|
-
portless
|
|
942
|
+
portless proxy start # Start proxy on port 1355
|
|
943
|
+
portless proxy start --https # Start with HTTPS/2 (faster page loads)
|
|
944
|
+
portless myapp next dev # -> http://myapp.localhost:1355
|
|
945
|
+
portless api.myapp pnpm start # -> http://api.myapp.localhost:1355
|
|
404
946
|
|
|
405
947
|
${chalk.bold("In package.json:")}
|
|
406
948
|
{
|
|
@@ -410,14 +952,30 @@ ${chalk.bold("In package.json:")}
|
|
|
410
952
|
}
|
|
411
953
|
|
|
412
954
|
${chalk.bold("How it works:")}
|
|
413
|
-
1. Start the proxy once
|
|
414
|
-
2. Run your apps - they register automatically
|
|
415
|
-
3. Access via http://<name>.localhost
|
|
955
|
+
1. Start the proxy once (listens on port 1355 by default, no sudo needed)
|
|
956
|
+
2. Run your apps - they auto-start the proxy and register automatically
|
|
957
|
+
3. Access via http://<name>.localhost:1355
|
|
416
958
|
4. .localhost domains auto-resolve to 127.0.0.1
|
|
417
959
|
|
|
960
|
+
${chalk.bold("HTTP/2 + HTTPS:")}
|
|
961
|
+
Use --https for HTTP/2 multiplexing (faster dev server page loads).
|
|
962
|
+
On first use, portless generates a local CA and adds it to your
|
|
963
|
+
system trust store. No browser warnings. No sudo required on macOS.
|
|
964
|
+
|
|
418
965
|
${chalk.bold("Options:")}
|
|
419
|
-
--port <number>
|
|
420
|
-
Ports
|
|
966
|
+
-p, --port <number> Port for the proxy to listen on (default: 1355)
|
|
967
|
+
Ports < 1024 require sudo
|
|
968
|
+
--https Enable HTTP/2 + TLS with auto-generated certs
|
|
969
|
+
--cert <path> Use a custom TLS certificate (implies --https)
|
|
970
|
+
--key <path> Use a custom TLS private key (implies --https)
|
|
971
|
+
--no-tls Disable HTTPS (overrides PORTLESS_HTTPS)
|
|
972
|
+
--foreground Run proxy in foreground (for debugging)
|
|
973
|
+
|
|
974
|
+
${chalk.bold("Environment variables:")}
|
|
975
|
+
PORTLESS_PORT=<number> Override the default proxy port (e.g. in .bashrc)
|
|
976
|
+
PORTLESS_HTTPS=1 Always enable HTTPS (set in .bashrc / .zshrc)
|
|
977
|
+
PORTLESS_STATE_DIR=<path> Override the state directory
|
|
978
|
+
PORTLESS=0 | PORTLESS=skip Run command directly without proxy
|
|
421
979
|
|
|
422
980
|
${chalk.bold("Skip portless:")}
|
|
423
981
|
PORTLESS=0 pnpm dev # Runs command directly without proxy
|
|
@@ -426,88 +984,245 @@ ${chalk.bold("Skip portless:")}
|
|
|
426
984
|
process.exit(0);
|
|
427
985
|
}
|
|
428
986
|
if (args[0] === "--version" || args[0] === "-v") {
|
|
429
|
-
console.log("0.
|
|
987
|
+
console.log("0.4.0");
|
|
430
988
|
process.exit(0);
|
|
431
989
|
}
|
|
990
|
+
if (args[0] === "trust") {
|
|
991
|
+
const { dir: dir2 } = await discoverState();
|
|
992
|
+
const result = trustCA(dir2);
|
|
993
|
+
if (result.trusted) {
|
|
994
|
+
console.log(chalk.green("Local CA added to system trust store."));
|
|
995
|
+
console.log(chalk.gray("Browsers will now trust portless HTTPS certificates."));
|
|
996
|
+
} else {
|
|
997
|
+
console.error(chalk.red(`Failed to trust CA: ${result.error}`));
|
|
998
|
+
if (result.error?.includes("sudo")) {
|
|
999
|
+
console.error(chalk.blue("Run with sudo:"));
|
|
1000
|
+
console.error(chalk.cyan(" sudo portless trust"));
|
|
1001
|
+
}
|
|
1002
|
+
process.exit(1);
|
|
1003
|
+
}
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
432
1006
|
if (args[0] === "list") {
|
|
433
|
-
|
|
1007
|
+
const { dir: dir2, port: port2, tls: tls3 } = await discoverState();
|
|
1008
|
+
const store2 = new RouteStore(dir2, {
|
|
1009
|
+
onWarning: (msg) => console.warn(chalk.yellow(msg))
|
|
1010
|
+
});
|
|
1011
|
+
listRoutes(store2, port2, tls3);
|
|
434
1012
|
return;
|
|
435
1013
|
}
|
|
436
1014
|
if (args[0] === "proxy") {
|
|
437
1015
|
if (args[1] === "stop") {
|
|
438
|
-
await
|
|
1016
|
+
const { dir: dir2, port: port2, tls: tls3 } = await discoverState();
|
|
1017
|
+
const store3 = new RouteStore(dir2, {
|
|
1018
|
+
onWarning: (msg) => console.warn(chalk.yellow(msg))
|
|
1019
|
+
});
|
|
1020
|
+
await stopProxy(store3, port2, tls3);
|
|
439
1021
|
return;
|
|
440
1022
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
1023
|
+
if (args[1] !== "start") {
|
|
1024
|
+
console.log(`
|
|
1025
|
+
${chalk.bold("Usage: portless proxy <command>")}
|
|
1026
|
+
|
|
1027
|
+
${chalk.cyan("portless proxy start")} Start the proxy (daemon)
|
|
1028
|
+
${chalk.cyan("portless proxy start --https")} Start with HTTP/2 + TLS
|
|
1029
|
+
${chalk.cyan("portless proxy start --foreground")} Start in foreground (for debugging)
|
|
1030
|
+
${chalk.cyan("portless proxy start -p 80")} Start on port 80 (requires sudo)
|
|
1031
|
+
${chalk.cyan("portless proxy stop")} Stop the proxy
|
|
1032
|
+
`);
|
|
1033
|
+
process.exit(args[1] ? 1 : 0);
|
|
1034
|
+
}
|
|
1035
|
+
const isForeground = args.includes("--foreground");
|
|
1036
|
+
let proxyPort = getDefaultPort();
|
|
1037
|
+
let portFlagIndex = args.indexOf("--port");
|
|
1038
|
+
if (portFlagIndex === -1) portFlagIndex = args.indexOf("-p");
|
|
444
1039
|
if (portFlagIndex !== -1) {
|
|
445
1040
|
const portValue = args[portFlagIndex + 1];
|
|
446
1041
|
if (!portValue || portValue.startsWith("-")) {
|
|
447
|
-
console.error(chalk.red("Error: --port requires a port number"));
|
|
448
|
-
console.
|
|
1042
|
+
console.error(chalk.red("Error: --port / -p requires a port number."));
|
|
1043
|
+
console.error(chalk.blue("Usage:"));
|
|
1044
|
+
console.error(chalk.cyan(" portless proxy start -p 8080"));
|
|
449
1045
|
process.exit(1);
|
|
450
1046
|
}
|
|
451
1047
|
proxyPort = parseInt(portValue, 10);
|
|
452
1048
|
if (isNaN(proxyPort) || proxyPort < 1 || proxyPort > 65535) {
|
|
453
1049
|
console.error(chalk.red(`Error: Invalid port number: ${portValue}`));
|
|
454
|
-
console.
|
|
1050
|
+
console.error(chalk.blue("Port must be between 1 and 65535."));
|
|
455
1051
|
process.exit(1);
|
|
456
1052
|
}
|
|
457
1053
|
}
|
|
1054
|
+
const hasNoTls = args.includes("--no-tls");
|
|
1055
|
+
const hasHttpsFlag = args.includes("--https");
|
|
1056
|
+
const wantHttps = !hasNoTls && (hasHttpsFlag || isHttpsEnvEnabled());
|
|
1057
|
+
let customCertPath = null;
|
|
1058
|
+
let customKeyPath = null;
|
|
1059
|
+
const certIdx = args.indexOf("--cert");
|
|
1060
|
+
if (certIdx !== -1) {
|
|
1061
|
+
customCertPath = args[certIdx + 1] || null;
|
|
1062
|
+
if (!customCertPath || customCertPath.startsWith("-")) {
|
|
1063
|
+
console.error(chalk.red("Error: --cert requires a file path."));
|
|
1064
|
+
process.exit(1);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
const keyIdx = args.indexOf("--key");
|
|
1068
|
+
if (keyIdx !== -1) {
|
|
1069
|
+
customKeyPath = args[keyIdx + 1] || null;
|
|
1070
|
+
if (!customKeyPath || customKeyPath.startsWith("-")) {
|
|
1071
|
+
console.error(chalk.red("Error: --key requires a file path."));
|
|
1072
|
+
process.exit(1);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
if (customCertPath && !customKeyPath || !customCertPath && customKeyPath) {
|
|
1076
|
+
console.error(chalk.red("Error: --cert and --key must be used together."));
|
|
1077
|
+
process.exit(1);
|
|
1078
|
+
}
|
|
1079
|
+
const useHttps = wantHttps || !!(customCertPath && customKeyPath);
|
|
1080
|
+
const stateDir = resolveStateDir(proxyPort);
|
|
1081
|
+
const store2 = new RouteStore(stateDir, {
|
|
1082
|
+
onWarning: (msg) => console.warn(chalk.yellow(msg))
|
|
1083
|
+
});
|
|
458
1084
|
if (await isProxyRunning(proxyPort)) {
|
|
459
|
-
if (
|
|
460
|
-
|
|
461
|
-
console.log(chalk.blue("To restart: portless proxy stop && sudo portless proxy"));
|
|
1085
|
+
if (isForeground) {
|
|
1086
|
+
return;
|
|
462
1087
|
}
|
|
1088
|
+
const needsSudo = proxyPort < PRIVILEGED_PORT_THRESHOLD;
|
|
1089
|
+
const sudoPrefix = needsSudo ? "sudo " : "";
|
|
1090
|
+
console.log(chalk.yellow(`Proxy is already running on port ${proxyPort}.`));
|
|
1091
|
+
console.log(
|
|
1092
|
+
chalk.blue(`To restart: portless proxy stop && ${sudoPrefix}portless proxy start`)
|
|
1093
|
+
);
|
|
463
1094
|
return;
|
|
464
1095
|
}
|
|
465
|
-
if (proxyPort <
|
|
466
|
-
console.error(chalk.red(`Error:
|
|
467
|
-
console.
|
|
1096
|
+
if (proxyPort < PRIVILEGED_PORT_THRESHOLD && (process.getuid?.() ?? -1) !== 0) {
|
|
1097
|
+
console.error(chalk.red(`Error: Port ${proxyPort} requires sudo.`));
|
|
1098
|
+
console.error(chalk.blue("Either run with sudo:"));
|
|
1099
|
+
console.error(chalk.cyan(" sudo portless proxy start -p 80"));
|
|
1100
|
+
console.error(chalk.blue("Or use the default port (no sudo needed):"));
|
|
1101
|
+
console.error(chalk.cyan(" portless proxy start"));
|
|
468
1102
|
process.exit(1);
|
|
469
1103
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
1104
|
+
let tlsOptions;
|
|
1105
|
+
if (useHttps) {
|
|
1106
|
+
store2.ensureDir();
|
|
1107
|
+
if (customCertPath && customKeyPath) {
|
|
1108
|
+
try {
|
|
1109
|
+
const cert = fs3.readFileSync(customCertPath);
|
|
1110
|
+
const key = fs3.readFileSync(customKeyPath);
|
|
1111
|
+
const certStr = cert.toString("utf-8");
|
|
1112
|
+
const keyStr = key.toString("utf-8");
|
|
1113
|
+
if (!certStr.includes("-----BEGIN CERTIFICATE-----")) {
|
|
1114
|
+
console.error(chalk.red(`Error: ${customCertPath} is not a valid PEM certificate.`));
|
|
1115
|
+
console.error(chalk.gray("Expected a file starting with -----BEGIN CERTIFICATE-----"));
|
|
1116
|
+
process.exit(1);
|
|
1117
|
+
}
|
|
1118
|
+
if (!keyStr.match(/-----BEGIN [\w\s]*PRIVATE KEY-----/)) {
|
|
1119
|
+
console.error(chalk.red(`Error: ${customKeyPath} is not a valid PEM private key.`));
|
|
1120
|
+
console.error(
|
|
1121
|
+
chalk.gray("Expected a file starting with -----BEGIN ...PRIVATE KEY-----")
|
|
1122
|
+
);
|
|
1123
|
+
process.exit(1);
|
|
1124
|
+
}
|
|
1125
|
+
tlsOptions = { cert, key };
|
|
1126
|
+
} catch (err) {
|
|
1127
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1128
|
+
console.error(chalk.red(`Error reading certificate files: ${message}`));
|
|
1129
|
+
process.exit(1);
|
|
1130
|
+
}
|
|
1131
|
+
} else {
|
|
1132
|
+
console.log(chalk.gray("Ensuring TLS certificates..."));
|
|
1133
|
+
const certs = ensureCerts(stateDir);
|
|
1134
|
+
if (certs.caGenerated) {
|
|
1135
|
+
console.log(chalk.green("Generated local CA certificate."));
|
|
1136
|
+
}
|
|
1137
|
+
if (!isCATrusted(stateDir)) {
|
|
1138
|
+
console.log(chalk.yellow("Adding CA to system trust store..."));
|
|
1139
|
+
const trustResult = trustCA(stateDir);
|
|
1140
|
+
if (trustResult.trusted) {
|
|
1141
|
+
console.log(
|
|
1142
|
+
chalk.green("CA added to system trust store. Browsers will trust portless certs.")
|
|
1143
|
+
);
|
|
1144
|
+
} else {
|
|
1145
|
+
console.warn(chalk.yellow("Could not add CA to system trust store."));
|
|
1146
|
+
if (trustResult.error) {
|
|
1147
|
+
console.warn(chalk.gray(trustResult.error));
|
|
1148
|
+
}
|
|
1149
|
+
console.warn(
|
|
1150
|
+
chalk.yellow("Browsers will show certificate warnings. To fix this later, run:")
|
|
1151
|
+
);
|
|
1152
|
+
console.warn(chalk.cyan(" portless trust"));
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
const cert = fs3.readFileSync(certs.certPath);
|
|
1156
|
+
const key = fs3.readFileSync(certs.keyPath);
|
|
1157
|
+
tlsOptions = {
|
|
1158
|
+
cert,
|
|
1159
|
+
key,
|
|
1160
|
+
SNICallback: createSNICallback(stateDir, cert, key)
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
if (isForeground) {
|
|
1165
|
+
console.log(chalk.blue.bold("\nportless proxy\n"));
|
|
1166
|
+
startProxyServer(store2, proxyPort, tlsOptions);
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
store2.ensureDir();
|
|
1170
|
+
const logPath = path3.join(stateDir, "proxy.log");
|
|
1171
|
+
const logFd = fs3.openSync(logPath, "a");
|
|
1172
|
+
try {
|
|
474
1173
|
try {
|
|
475
|
-
|
|
1174
|
+
fs3.chmodSync(logPath, FILE_MODE);
|
|
476
1175
|
} catch {
|
|
477
1176
|
}
|
|
478
|
-
const daemonArgs = [process.argv[1], "proxy"];
|
|
1177
|
+
const daemonArgs = [process.argv[1], "proxy", "start", "--foreground"];
|
|
479
1178
|
if (portFlagIndex !== -1) {
|
|
480
1179
|
daemonArgs.push("--port", proxyPort.toString());
|
|
481
1180
|
}
|
|
482
|
-
|
|
1181
|
+
if (useHttps) {
|
|
1182
|
+
if (customCertPath && customKeyPath) {
|
|
1183
|
+
daemonArgs.push("--cert", customCertPath, "--key", customKeyPath);
|
|
1184
|
+
} else {
|
|
1185
|
+
daemonArgs.push("--https");
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
const child = spawn2(process.execPath, daemonArgs, {
|
|
483
1189
|
detached: true,
|
|
484
1190
|
stdio: ["ignore", logFd, logFd],
|
|
485
1191
|
env: process.env
|
|
486
1192
|
});
|
|
487
1193
|
child.unref();
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
1194
|
+
} finally {
|
|
1195
|
+
fs3.closeSync(logFd);
|
|
1196
|
+
}
|
|
1197
|
+
if (!await waitForProxy(proxyPort, void 0, void 0, useHttps)) {
|
|
1198
|
+
console.error(chalk.red("Proxy failed to start (timed out waiting for it to listen)."));
|
|
1199
|
+
console.error(chalk.blue("Try starting the proxy in the foreground to see the error:"));
|
|
1200
|
+
const needsSudo = proxyPort < PRIVILEGED_PORT_THRESHOLD;
|
|
1201
|
+
console.error(chalk.cyan(` ${needsSudo ? "sudo " : ""}portless proxy start --foreground`));
|
|
1202
|
+
if (fs3.existsSync(logPath)) {
|
|
1203
|
+
console.error(chalk.gray(`Logs: ${logPath}`));
|
|
495
1204
|
}
|
|
496
|
-
|
|
1205
|
+
process.exit(1);
|
|
497
1206
|
}
|
|
498
|
-
|
|
499
|
-
|
|
1207
|
+
const proto = useHttps ? "HTTPS/2" : "HTTP";
|
|
1208
|
+
console.log(chalk.green(`${proto} proxy started on port ${proxyPort}`));
|
|
500
1209
|
return;
|
|
501
1210
|
}
|
|
502
1211
|
const name = args[0];
|
|
503
1212
|
const commandArgs = args.slice(1);
|
|
504
1213
|
if (commandArgs.length === 0) {
|
|
505
|
-
console.error(chalk.red("Error: No command provided"));
|
|
506
|
-
console.
|
|
507
|
-
console.
|
|
1214
|
+
console.error(chalk.red("Error: No command provided."));
|
|
1215
|
+
console.error(chalk.blue("Usage:"));
|
|
1216
|
+
console.error(chalk.cyan(" portless <name> <command...>"));
|
|
1217
|
+
console.error(chalk.blue("Example:"));
|
|
1218
|
+
console.error(chalk.cyan(" portless myapp next dev"));
|
|
508
1219
|
process.exit(1);
|
|
509
1220
|
}
|
|
510
|
-
await
|
|
1221
|
+
const { dir, port, tls: tls2 } = await discoverState();
|
|
1222
|
+
const store = new RouteStore(dir, {
|
|
1223
|
+
onWarning: (msg) => console.warn(chalk.yellow(msg))
|
|
1224
|
+
});
|
|
1225
|
+
await runApp(store, port, dir, name, commandArgs, tls2);
|
|
511
1226
|
}
|
|
512
1227
|
main().catch((err) => {
|
|
513
1228
|
const message = err instanceof Error ? err.message : String(err);
|