opentunnel-cli 1.0.23 → 1.0.25
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 +39 -2
- package/dist/cli/index.js +678 -84
- package/dist/cli/index.js.map +1 -1
- package/dist/client/CloudflareTunnelClient.d.ts +110 -0
- package/dist/client/CloudflareTunnelClient.d.ts.map +1 -0
- package/dist/client/CloudflareTunnelClient.js +531 -0
- package/dist/client/CloudflareTunnelClient.js.map +1 -0
- package/dist/client/NgrokClient.d.ts +18 -1
- package/dist/client/NgrokClient.d.ts.map +1 -1
- package/dist/client/NgrokClient.js +130 -4
- package/dist/client/NgrokClient.js.map +1 -1
- package/dist/client/TunnelClient.d.ts +0 -1
- package/dist/client/TunnelClient.d.ts.map +1 -1
- package/dist/client/TunnelClient.js +2 -96
- package/dist/client/TunnelClient.js.map +1 -1
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +3 -1
- package/dist/client/index.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/index.d.ts +2 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +18 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/pages/index.d.ts +2 -0
- package/dist/lib/pages/index.d.ts.map +1 -0
- package/dist/lib/pages/index.js +6 -0
- package/dist/lib/pages/index.js.map +1 -0
- package/dist/lib/pages/not-running.d.ts +10 -0
- package/dist/lib/pages/not-running.d.ts.map +1 -0
- package/dist/lib/pages/not-running.js +117 -0
- package/dist/lib/pages/not-running.js.map +1 -0
- package/dist/server/TunnelServer.d.ts +1 -3
- package/dist/server/TunnelServer.d.ts.map +1 -1
- package/dist/server/TunnelServer.js +7 -58
- package/dist/server/TunnelServer.js.map +1 -1
- package/dist/shared/credentials.d.ts +101 -0
- package/dist/shared/credentials.d.ts.map +1 -0
- package/dist/shared/credentials.js +302 -0
- package/dist/shared/credentials.js.map +1 -0
- package/dist/shared/ip-filter.d.ts +75 -0
- package/dist/shared/ip-filter.d.ts.map +1 -0
- package/dist/shared/ip-filter.js +203 -0
- package/dist/shared/ip-filter.js.map +1 -0
- package/dist/shared/types.d.ts +34 -0
- package/dist/shared/types.d.ts.map +1 -1
- package/package.json +6 -3
package/dist/cli/index.js
CHANGED
|
@@ -42,6 +42,8 @@ const chalk_1 = __importDefault(require("chalk"));
|
|
|
42
42
|
const ora_1 = __importDefault(require("ora"));
|
|
43
43
|
const TunnelClient_1 = require("../client/TunnelClient");
|
|
44
44
|
const NgrokClient_1 = require("../client/NgrokClient");
|
|
45
|
+
const CloudflareTunnelClient_1 = require("../client/CloudflareTunnelClient");
|
|
46
|
+
const credentials_1 = require("../shared/credentials");
|
|
45
47
|
const utils_1 = require("../shared/utils");
|
|
46
48
|
const yaml_1 = require("yaml");
|
|
47
49
|
const fs = __importStar(require("fs"));
|
|
@@ -220,8 +222,8 @@ const program = new commander_1.Command();
|
|
|
220
222
|
program
|
|
221
223
|
.name("opentunnel")
|
|
222
224
|
.alias("ot")
|
|
223
|
-
.description("Expose local ports to the internet via custom domains or
|
|
224
|
-
.version("1.0.
|
|
225
|
+
.description("Expose local ports to the internet via custom domains, ngrok, or Cloudflare Tunnel")
|
|
226
|
+
.version("1.0.25");
|
|
225
227
|
// Helper function to build WebSocket URL from domain
|
|
226
228
|
// User only provides base domain (e.g., fjrg2007.com), system handles the rest
|
|
227
229
|
// Note: --insecure flag only affects certificate verification, not the protocol
|
|
@@ -618,8 +620,28 @@ program
|
|
|
618
620
|
.option("--insecure", "Skip SSL verification (for self-signed certs)")
|
|
619
621
|
.option("--ngrok", "Use ngrok instead of OpenTunnel server")
|
|
620
622
|
.option("--region <region>", "Ngrok region (us, eu, ap, au, sa, jp, in)", "us")
|
|
623
|
+
.option("--cloudflare, --cf", "Use Cloudflare Tunnel instead of OpenTunnel server")
|
|
624
|
+
.option("--cf-hostname <hostname>", "Custom hostname for Cloudflare Tunnel")
|
|
625
|
+
.option("--provider <provider>", "Tunnel provider (opentunnel, ngrok, cloudflare)")
|
|
621
626
|
.action(async (port, options) => {
|
|
622
|
-
|
|
627
|
+
// Determine provider
|
|
628
|
+
const provider = options.provider?.toLowerCase() ||
|
|
629
|
+
(options.cloudflare || options.cf ? "cloudflare" : null) ||
|
|
630
|
+
(options.ngrok ? "ngrok" : null);
|
|
631
|
+
// Cloudflare Tunnel
|
|
632
|
+
if (provider === "cloudflare" || provider === "cf") {
|
|
633
|
+
await createCloudflareTunnel({
|
|
634
|
+
protocol: options.https ? "https" : "http",
|
|
635
|
+
localHost: options.host,
|
|
636
|
+
localPort: parseInt(port),
|
|
637
|
+
hostname: options.cfHostname,
|
|
638
|
+
tunnelName: options.subdomain, // -n works as tunnel name for CF
|
|
639
|
+
noTlsVerify: options.insecure,
|
|
640
|
+
});
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
// ngrok
|
|
644
|
+
if (provider === "ngrok") {
|
|
623
645
|
await createNgrokTunnel({
|
|
624
646
|
protocol: options.https ? "https" : "http",
|
|
625
647
|
localHost: options.host,
|
|
@@ -630,8 +652,8 @@ program
|
|
|
630
652
|
});
|
|
631
653
|
return;
|
|
632
654
|
}
|
|
633
|
-
// If remote server domain provided,
|
|
634
|
-
if (options.domain) {
|
|
655
|
+
// If remote server domain provided, connect to OpenTunnel server
|
|
656
|
+
if (options.domain && options.domain !== "cloudflare" && options.domain !== "ngrok") {
|
|
635
657
|
const { url: serverUrl } = buildServerUrl(options.domain, options.basePath);
|
|
636
658
|
await createTunnel({
|
|
637
659
|
protocol: options.https ? "https" : "http",
|
|
@@ -649,6 +671,8 @@ program
|
|
|
649
671
|
console.log(chalk_1.default.gray("\nExamples:"));
|
|
650
672
|
console.log(chalk_1.default.cyan(" opentunnel http 3000 -s example.com"));
|
|
651
673
|
console.log(chalk_1.default.cyan(" opentunnel http 3000 --domain example.com -n myapp"));
|
|
674
|
+
console.log(chalk_1.default.cyan(" opentunnel http 3000 --cloudflare"));
|
|
675
|
+
console.log(chalk_1.default.cyan(" opentunnel http 3000 --ngrok"));
|
|
652
676
|
process.exit(1);
|
|
653
677
|
});
|
|
654
678
|
// TCP tunnel command
|
|
@@ -664,8 +688,33 @@ program
|
|
|
664
688
|
.option("--insecure", "Skip SSL verification (for self-signed certs)")
|
|
665
689
|
.option("--ngrok", "Use ngrok instead of OpenTunnel server")
|
|
666
690
|
.option("--region <region>", "Ngrok region (us, eu, ap, au, sa, jp, in)", "us")
|
|
691
|
+
.option("--cloudflare, --cf", "Use Cloudflare Tunnel (TCP requires named tunnel)")
|
|
692
|
+
.option("--cf-hostname <hostname>", "Custom hostname for Cloudflare Tunnel")
|
|
693
|
+
.option("--provider <provider>", "Tunnel provider (opentunnel, ngrok, cloudflare)")
|
|
667
694
|
.action(async (port, options) => {
|
|
668
|
-
|
|
695
|
+
// Determine provider
|
|
696
|
+
const provider = options.provider?.toLowerCase() ||
|
|
697
|
+
(options.cloudflare || options.cf ? "cloudflare" : null) ||
|
|
698
|
+
(options.ngrok ? "ngrok" : null);
|
|
699
|
+
// Cloudflare Tunnel - Note: TCP requires named tunnel
|
|
700
|
+
if (provider === "cloudflare" || provider === "cf") {
|
|
701
|
+
if (!options.subdomain) {
|
|
702
|
+
console.log(chalk_1.default.yellow("Note: Cloudflare TCP tunnels require a named tunnel."));
|
|
703
|
+
console.log(chalk_1.default.gray("Use -n <tunnel-name> to specify a named tunnel."));
|
|
704
|
+
console.log(chalk_1.default.gray("\nExample: opentunnel tcp 5432 --cf -n my-tunnel"));
|
|
705
|
+
}
|
|
706
|
+
await createCloudflareTunnel({
|
|
707
|
+
protocol: "tcp",
|
|
708
|
+
localHost: options.host,
|
|
709
|
+
localPort: parseInt(port),
|
|
710
|
+
hostname: options.cfHostname,
|
|
711
|
+
tunnelName: options.subdomain,
|
|
712
|
+
noTlsVerify: options.insecure,
|
|
713
|
+
});
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
// ngrok
|
|
717
|
+
if (provider === "ngrok") {
|
|
669
718
|
await createNgrokTunnel({
|
|
670
719
|
protocol: "tcp",
|
|
671
720
|
localHost: options.host,
|
|
@@ -676,8 +725,8 @@ program
|
|
|
676
725
|
});
|
|
677
726
|
return;
|
|
678
727
|
}
|
|
679
|
-
// If remote server domain provided,
|
|
680
|
-
if (options.domain) {
|
|
728
|
+
// If remote server domain provided, connect to OpenTunnel server
|
|
729
|
+
if (options.domain && options.domain !== "cloudflare" && options.domain !== "ngrok") {
|
|
681
730
|
const { url: serverUrl } = buildServerUrl(options.domain, options.basePath);
|
|
682
731
|
await createTunnel({
|
|
683
732
|
protocol: "tcp",
|
|
@@ -695,6 +744,7 @@ program
|
|
|
695
744
|
console.log(chalk_1.default.gray("\nExamples:"));
|
|
696
745
|
console.log(chalk_1.default.cyan(" opentunnel tcp 5432 -s example.com"));
|
|
697
746
|
console.log(chalk_1.default.cyan(" opentunnel tcp 5432 --domain example.com -r 15432"));
|
|
747
|
+
console.log(chalk_1.default.cyan(" opentunnel tcp 5432 --ngrok"));
|
|
698
748
|
process.exit(1);
|
|
699
749
|
});
|
|
700
750
|
// Quick expose command
|
|
@@ -709,6 +759,7 @@ program
|
|
|
709
759
|
.option("--domain <domain>", "Server domain (e.g., domain.com)")
|
|
710
760
|
.option("--insecure", "Skip SSL certificate verification (for self-signed certs)")
|
|
711
761
|
.option("--ngrok", "Use ngrok instead of OpenTunnel server")
|
|
762
|
+
.option("--cloudflare, --cf", "Use Cloudflare Tunnel instead of OpenTunnel server")
|
|
712
763
|
.action(async (port, options) => {
|
|
713
764
|
const serverUrl = options.server || (options.domain
|
|
714
765
|
? `wss://${options.domain}/_tunnel`
|
|
@@ -717,26 +768,37 @@ program
|
|
|
717
768
|
await runTunnelInBackground("expose", port, { ...options, server: serverUrl });
|
|
718
769
|
return;
|
|
719
770
|
}
|
|
720
|
-
|
|
721
|
-
|
|
771
|
+
// Cloudflare Tunnel
|
|
772
|
+
if (options.cloudflare || options.cf || options.server === "cloudflare") {
|
|
773
|
+
await createCloudflareTunnel({
|
|
722
774
|
protocol: options.protocol,
|
|
723
775
|
localHost: "localhost",
|
|
724
776
|
localPort: parseInt(port),
|
|
725
|
-
|
|
726
|
-
authtoken: options.token
|
|
777
|
+
noTlsVerify: options.insecure,
|
|
727
778
|
});
|
|
779
|
+
return;
|
|
728
780
|
}
|
|
729
|
-
|
|
730
|
-
|
|
781
|
+
// ngrok
|
|
782
|
+
if (options.ngrok || options.server === "ngrok") {
|
|
783
|
+
await createNgrokTunnel({
|
|
731
784
|
protocol: options.protocol,
|
|
732
785
|
localHost: "localhost",
|
|
733
786
|
localPort: parseInt(port),
|
|
734
787
|
subdomain: options.subdomain,
|
|
735
|
-
|
|
736
|
-
token: options.token,
|
|
737
|
-
insecure: options.insecure
|
|
788
|
+
authtoken: options.token
|
|
738
789
|
});
|
|
790
|
+
return;
|
|
739
791
|
}
|
|
792
|
+
// OpenTunnel server
|
|
793
|
+
await createTunnel({
|
|
794
|
+
protocol: options.protocol,
|
|
795
|
+
localHost: "localhost",
|
|
796
|
+
localPort: parseInt(port),
|
|
797
|
+
subdomain: options.subdomain,
|
|
798
|
+
serverUrl,
|
|
799
|
+
token: options.token,
|
|
800
|
+
insecure: options.insecure
|
|
801
|
+
});
|
|
740
802
|
});
|
|
741
803
|
// Server command
|
|
742
804
|
program
|
|
@@ -1613,7 +1675,7 @@ program
|
|
|
1613
1675
|
console.log(chalk_1.default.cyan(`Connecting to ${remote}...\n`));
|
|
1614
1676
|
console.log(chalk_1.default.cyan(`Starting ${tunnelsToStart.length} tunnel(s)...\n`));
|
|
1615
1677
|
try {
|
|
1616
|
-
await startTunnelsFromConfig(tunnelsToStart, serverUrl, config.server?.token, true);
|
|
1678
|
+
await startTunnelsFromConfig(tunnelsToStart, serverUrl, config.server?.token, true, config);
|
|
1617
1679
|
console.log(chalk_1.default.gray("\nPress Ctrl+C to stop"));
|
|
1618
1680
|
// Keep running
|
|
1619
1681
|
await new Promise(() => { });
|
|
@@ -1665,7 +1727,7 @@ program
|
|
|
1665
1727
|
const serverUrl = `${wsProtocol}://localhost:${port}/_tunnel`;
|
|
1666
1728
|
// Small delay to ensure server is fully ready
|
|
1667
1729
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
1668
|
-
await startTunnelsFromConfig(tunnelsToStart, serverUrl, config.server?.token, true);
|
|
1730
|
+
await startTunnelsFromConfig(tunnelsToStart, serverUrl, config.server?.token, true, config);
|
|
1669
1731
|
}
|
|
1670
1732
|
else if (isServerMode) {
|
|
1671
1733
|
console.log(chalk_1.default.gray("\nServer ready. Waiting for connections..."));
|
|
@@ -2301,79 +2363,175 @@ async function runTunnelInBackgroundFromConfig(name, protocol, port, options) {
|
|
|
2301
2363
|
console.log(chalk_1.default.green(` ✓ ${name}: started (PID: ${child.pid})`));
|
|
2302
2364
|
}
|
|
2303
2365
|
;
|
|
2304
|
-
async function startTunnelsFromConfig(tunnels, serverUrl, token, insecure) {
|
|
2305
|
-
|
|
2306
|
-
const
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
try {
|
|
2314
|
-
await client.connect();
|
|
2315
|
-
spinner.succeed("Connected to server");
|
|
2316
|
-
const activeTunnels = [];
|
|
2317
|
-
for (const tunnel of tunnels) {
|
|
2318
|
-
const tunnelSpinner = (0, ora_1.default)(`Creating tunnel: ${tunnel.name}...`).start();
|
|
2319
|
-
try {
|
|
2320
|
-
const { tunnelId, publicUrl } = await client.createTunnel({
|
|
2321
|
-
protocol: tunnel.protocol,
|
|
2322
|
-
localHost: tunnel.host || "localhost",
|
|
2323
|
-
localPort: tunnel.port,
|
|
2324
|
-
subdomain: tunnel.subdomain,
|
|
2325
|
-
remotePort: tunnel.remotePort,
|
|
2326
|
-
});
|
|
2327
|
-
activeTunnels.push({ name: tunnel.name, tunnelId, publicUrl });
|
|
2328
|
-
tunnelSpinner.succeed(`${tunnel.name}: ${publicUrl}`);
|
|
2329
|
-
}
|
|
2330
|
-
catch (err) {
|
|
2331
|
-
tunnelSpinner.fail(`${tunnel.name}: ${err.message}`);
|
|
2332
|
-
}
|
|
2366
|
+
async function startTunnelsFromConfig(tunnels, serverUrl, token, insecure, globalConfig) {
|
|
2367
|
+
// Determine global provider
|
|
2368
|
+
const globalProvider = globalConfig?.provider || "opentunnel";
|
|
2369
|
+
// Group tunnels by provider
|
|
2370
|
+
const tunnelsByProvider = new Map();
|
|
2371
|
+
for (const tunnel of tunnels) {
|
|
2372
|
+
const provider = tunnel.provider || globalProvider;
|
|
2373
|
+
if (!tunnelsByProvider.has(provider)) {
|
|
2374
|
+
tunnelsByProvider.set(provider, []);
|
|
2333
2375
|
}
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2376
|
+
tunnelsByProvider.get(provider).push(tunnel);
|
|
2377
|
+
}
|
|
2378
|
+
const activeTunnels = [];
|
|
2379
|
+
const clients = [];
|
|
2380
|
+
// Process OpenTunnel tunnels
|
|
2381
|
+
const opentunnelTunnels = tunnelsByProvider.get("opentunnel") || [];
|
|
2382
|
+
if (opentunnelTunnels.length > 0) {
|
|
2383
|
+
const spinner = (0, ora_1.default)("Connecting to OpenTunnel server...").start();
|
|
2384
|
+
const client = new TunnelClient_1.TunnelClient({
|
|
2385
|
+
serverUrl,
|
|
2386
|
+
token,
|
|
2387
|
+
reconnect: true,
|
|
2388
|
+
silent: true,
|
|
2389
|
+
rejectUnauthorized: !insecure,
|
|
2390
|
+
});
|
|
2391
|
+
try {
|
|
2392
|
+
await client.connect();
|
|
2393
|
+
spinner.succeed("Connected to OpenTunnel server");
|
|
2394
|
+
clients.push({ provider: "opentunnel", client });
|
|
2395
|
+
for (const tunnel of opentunnelTunnels) {
|
|
2396
|
+
const tunnelSpinner = (0, ora_1.default)(`Creating tunnel: ${tunnel.name}...`).start();
|
|
2397
|
+
try {
|
|
2398
|
+
const { tunnelId, publicUrl } = await client.createTunnel({
|
|
2399
|
+
protocol: tunnel.protocol,
|
|
2400
|
+
localHost: tunnel.host || "localhost",
|
|
2401
|
+
localPort: tunnel.port,
|
|
2402
|
+
subdomain: tunnel.subdomain,
|
|
2403
|
+
remotePort: tunnel.remotePort,
|
|
2404
|
+
});
|
|
2405
|
+
activeTunnels.push({ name: tunnel.name, tunnelId, publicUrl, provider: "opentunnel", client });
|
|
2406
|
+
tunnelSpinner.succeed(`${tunnel.name}: ${publicUrl}`);
|
|
2407
|
+
}
|
|
2408
|
+
catch (err) {
|
|
2409
|
+
tunnelSpinner.fail(`${tunnel.name}: ${err.message}`);
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
// Handle reconnection events
|
|
2413
|
+
client.on("disconnected", () => {
|
|
2414
|
+
console.log(chalk_1.default.yellow("\n OpenTunnel disconnected, reconnecting..."));
|
|
2415
|
+
});
|
|
2416
|
+
client.on("connected", () => {
|
|
2417
|
+
console.log(chalk_1.default.green(" OpenTunnel reconnected!"));
|
|
2418
|
+
});
|
|
2337
2419
|
}
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
console.log(chalk_1.default.cyan("─────────────────────────────────────────\n"));
|
|
2341
|
-
for (const t of activeTunnels) {
|
|
2342
|
-
console.log(` ${chalk_1.default.white(t.name.padEnd(15))} ${chalk_1.default.green(t.publicUrl)}`);
|
|
2420
|
+
catch (err) {
|
|
2421
|
+
spinner.fail(`OpenTunnel connection failed: ${err.message}`);
|
|
2343
2422
|
}
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
//
|
|
2352
|
-
const
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
}
|
|
2359
|
-
await client.disconnect();
|
|
2360
|
-
closeSpinner.succeed("All tunnels closed");
|
|
2361
|
-
process.exit(0);
|
|
2362
|
-
};
|
|
2363
|
-
process.on("SIGINT", cleanup);
|
|
2364
|
-
process.on("SIGTERM", cleanup);
|
|
2365
|
-
// Handle reconnection
|
|
2366
|
-
client.on("disconnected", () => {
|
|
2367
|
-
console.log(chalk_1.default.yellow("\n Disconnected, reconnecting..."));
|
|
2423
|
+
}
|
|
2424
|
+
// Process ngrok tunnels (one at a time, ngrok limitation)
|
|
2425
|
+
const ngrokTunnels = tunnelsByProvider.get("ngrok") || [];
|
|
2426
|
+
for (const tunnel of ngrokTunnels) {
|
|
2427
|
+
const spinner = (0, ora_1.default)(`Creating ngrok tunnel: ${tunnel.name}...`).start();
|
|
2428
|
+
const ngrokToken = tunnel.ngrokToken || globalConfig?.ngrok?.token;
|
|
2429
|
+
const ngrokRegion = tunnel.ngrokRegion || globalConfig?.ngrok?.region || "us";
|
|
2430
|
+
// Merge IP access config: tunnel-specific > global security > none
|
|
2431
|
+
const { IpFilter } = await Promise.resolve().then(() => __importStar(require("../shared/ip-filter")));
|
|
2432
|
+
const ipAccess = IpFilter.mergeConfigs(globalConfig?.security?.ipAccess, tunnel.ipAccess);
|
|
2433
|
+
const client = new NgrokClient_1.NgrokClient({
|
|
2434
|
+
authtoken: ngrokToken,
|
|
2435
|
+
region: ngrokRegion,
|
|
2436
|
+
ipAccess,
|
|
2368
2437
|
});
|
|
2369
|
-
|
|
2370
|
-
|
|
2438
|
+
try {
|
|
2439
|
+
await client.connect();
|
|
2440
|
+
const { tunnelId, publicUrl } = await client.createTunnel({
|
|
2441
|
+
protocol: tunnel.protocol,
|
|
2442
|
+
localHost: tunnel.host || "localhost",
|
|
2443
|
+
localPort: tunnel.port,
|
|
2444
|
+
subdomain: tunnel.subdomain,
|
|
2445
|
+
remotePort: tunnel.remotePort,
|
|
2446
|
+
});
|
|
2447
|
+
activeTunnels.push({ name: tunnel.name, tunnelId, publicUrl, provider: "ngrok", client });
|
|
2448
|
+
clients.push({ provider: "ngrok", client });
|
|
2449
|
+
spinner.succeed(`${tunnel.name}: ${publicUrl} (ngrok)`);
|
|
2450
|
+
}
|
|
2451
|
+
catch (err) {
|
|
2452
|
+
spinner.fail(`${tunnel.name}: ${err.message}`);
|
|
2453
|
+
console.log(chalk_1.default.yellow(" Make sure ngrok is installed: https://ngrok.com/download"));
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
// Process Cloudflare tunnels (one at a time)
|
|
2457
|
+
const cloudflareTunnels = tunnelsByProvider.get("cloudflare") || [];
|
|
2458
|
+
for (const tunnel of cloudflareTunnels) {
|
|
2459
|
+
const spinner = (0, ora_1.default)(`Creating Cloudflare tunnel: ${tunnel.name}...`).start();
|
|
2460
|
+
const cfHostname = tunnel.cfHostname || globalConfig?.cloudflare?.hostname;
|
|
2461
|
+
// For Cloudflare: subdomain = tunnel name
|
|
2462
|
+
const cfTunnelName = tunnel.subdomain || globalConfig?.cloudflare?.tunnelName;
|
|
2463
|
+
// Merge IP access config: tunnel-specific > global security > none
|
|
2464
|
+
const { IpFilter } = await Promise.resolve().then(() => __importStar(require("../shared/ip-filter")));
|
|
2465
|
+
const ipAccess = IpFilter.mergeConfigs(globalConfig?.security?.ipAccess, tunnel.ipAccess);
|
|
2466
|
+
const client = new CloudflareTunnelClient_1.CloudflareTunnelClient({
|
|
2467
|
+
hostname: cfHostname,
|
|
2468
|
+
tunnelName: cfTunnelName,
|
|
2469
|
+
protocol: tunnel.protocol === "https" ? "https" : "http",
|
|
2470
|
+
ipAccess,
|
|
2371
2471
|
});
|
|
2472
|
+
try {
|
|
2473
|
+
await client.connect();
|
|
2474
|
+
const { tunnelId, publicUrl } = await client.createTunnel({
|
|
2475
|
+
protocol: tunnel.protocol,
|
|
2476
|
+
localHost: tunnel.host || "localhost",
|
|
2477
|
+
localPort: tunnel.port,
|
|
2478
|
+
});
|
|
2479
|
+
activeTunnels.push({ name: tunnel.name, tunnelId, publicUrl, provider: "cloudflare", client });
|
|
2480
|
+
clients.push({ provider: "cloudflare", client });
|
|
2481
|
+
const tunnelMode = cfTunnelName ? ` [${cfTunnelName}]` : "";
|
|
2482
|
+
spinner.succeed(`${tunnel.name}: ${publicUrl}${tunnelMode} (cloudflare)`);
|
|
2483
|
+
}
|
|
2484
|
+
catch (err) {
|
|
2485
|
+
spinner.fail(`${tunnel.name}: ${err.message}`);
|
|
2486
|
+
console.log(chalk_1.default.yellow(" cloudflared installs automatically. Check internet connection or try: npm rebuild cloudflared"));
|
|
2487
|
+
}
|
|
2372
2488
|
}
|
|
2373
|
-
|
|
2374
|
-
|
|
2489
|
+
if (activeTunnels.length === 0) {
|
|
2490
|
+
console.log(chalk_1.default.red("\nNo tunnels created"));
|
|
2375
2491
|
process.exit(1);
|
|
2376
2492
|
}
|
|
2493
|
+
console.log(chalk_1.default.cyan("\n─────────────────────────────────────────"));
|
|
2494
|
+
console.log(chalk_1.default.green(` ${activeTunnels.length} tunnel(s) active`));
|
|
2495
|
+
console.log(chalk_1.default.cyan("─────────────────────────────────────────\n"));
|
|
2496
|
+
for (const t of activeTunnels) {
|
|
2497
|
+
const providerTag = t.provider !== "opentunnel" ? chalk_1.default.gray(` (${t.provider})`) : "";
|
|
2498
|
+
console.log(` ${chalk_1.default.white(t.name.padEnd(15))} ${chalk_1.default.green(t.publicUrl)}${providerTag}`);
|
|
2499
|
+
}
|
|
2500
|
+
console.log(chalk_1.default.gray("\n Press Ctrl+C to stop all tunnels\n"));
|
|
2501
|
+
// Keep alive with uptime counter
|
|
2502
|
+
const startTime = Date.now();
|
|
2503
|
+
const statsInterval = setInterval(() => {
|
|
2504
|
+
const uptime = (0, utils_1.formatDuration)(Date.now() - startTime);
|
|
2505
|
+
process.stdout.write(`\r ${chalk_1.default.gray(`Uptime: ${uptime}`)}`);
|
|
2506
|
+
}, 1000);
|
|
2507
|
+
// Handle exit
|
|
2508
|
+
const cleanup = async () => {
|
|
2509
|
+
clearInterval(statsInterval);
|
|
2510
|
+
console.log("\n");
|
|
2511
|
+
const closeSpinner = (0, ora_1.default)("Closing tunnels...").start();
|
|
2512
|
+
for (const t of activeTunnels) {
|
|
2513
|
+
try {
|
|
2514
|
+
await t.client.closeTunnel(t.tunnelId);
|
|
2515
|
+
}
|
|
2516
|
+
catch {
|
|
2517
|
+
// Ignore errors during cleanup
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
for (const { client } of clients) {
|
|
2521
|
+
try {
|
|
2522
|
+
await client.disconnect();
|
|
2523
|
+
}
|
|
2524
|
+
catch {
|
|
2525
|
+
// Ignore errors during cleanup
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
closeSpinner.succeed("All tunnels closed");
|
|
2529
|
+
process.exit(0);
|
|
2530
|
+
};
|
|
2531
|
+
process.on("SIGINT", cleanup);
|
|
2532
|
+
process.on("SIGTERM", cleanup);
|
|
2533
|
+
// Keep process alive
|
|
2534
|
+
await new Promise(() => { });
|
|
2377
2535
|
}
|
|
2378
2536
|
;
|
|
2379
2537
|
async function runTunnelInBackground(command, port, options) {
|
|
@@ -2565,6 +2723,61 @@ async function createNgrokTunnel(options) {
|
|
|
2565
2723
|
}
|
|
2566
2724
|
}
|
|
2567
2725
|
;
|
|
2726
|
+
async function createCloudflareTunnel(options) {
|
|
2727
|
+
const tunnelType = options.tunnelName ? `named tunnel '${options.tunnelName}'` : "quick tunnel";
|
|
2728
|
+
const spinner = (0, ora_1.default)(`Starting Cloudflare ${tunnelType}...`).start();
|
|
2729
|
+
const client = new CloudflareTunnelClient_1.CloudflareTunnelClient({
|
|
2730
|
+
hostname: options.hostname,
|
|
2731
|
+
tunnelName: options.tunnelName,
|
|
2732
|
+
noTlsVerify: options.noTlsVerify,
|
|
2733
|
+
protocol: options.protocol === "https" ? "https" : "http",
|
|
2734
|
+
});
|
|
2735
|
+
try {
|
|
2736
|
+
await client.connect();
|
|
2737
|
+
spinner.text = "Creating tunnel...";
|
|
2738
|
+
const { tunnelId, publicUrl } = await client.createTunnel({
|
|
2739
|
+
protocol: options.protocol,
|
|
2740
|
+
localHost: options.localHost,
|
|
2741
|
+
localPort: options.localPort,
|
|
2742
|
+
});
|
|
2743
|
+
spinner.succeed("Tunnel established!");
|
|
2744
|
+
const providerInfo = options.tunnelName
|
|
2745
|
+
? `Cloudflare [${options.tunnelName}]`
|
|
2746
|
+
: "Cloudflare";
|
|
2747
|
+
printTunnelInfo({
|
|
2748
|
+
status: "Online",
|
|
2749
|
+
protocol: options.protocol,
|
|
2750
|
+
localHost: options.localHost,
|
|
2751
|
+
localPort: options.localPort,
|
|
2752
|
+
publicUrl,
|
|
2753
|
+
provider: providerInfo,
|
|
2754
|
+
});
|
|
2755
|
+
// Keep alive
|
|
2756
|
+
const startTime = Date.now();
|
|
2757
|
+
const statsInterval = setInterval(() => {
|
|
2758
|
+
const uptime = (0, utils_1.formatDuration)(Date.now() - startTime);
|
|
2759
|
+
process.stdout.write(`\r ${chalk_1.default.gray(`Uptime: ${uptime}`)}`);
|
|
2760
|
+
}, 1000);
|
|
2761
|
+
// Handle exit
|
|
2762
|
+
const cleanup = async () => {
|
|
2763
|
+
clearInterval(statsInterval);
|
|
2764
|
+
console.log("\n");
|
|
2765
|
+
spinner.start("Closing tunnel...");
|
|
2766
|
+
await client.closeTunnel(tunnelId);
|
|
2767
|
+
await client.disconnect();
|
|
2768
|
+
spinner.succeed("Tunnel closed");
|
|
2769
|
+
process.exit(0);
|
|
2770
|
+
};
|
|
2771
|
+
process.on("SIGINT", cleanup);
|
|
2772
|
+
process.on("SIGTERM", cleanup);
|
|
2773
|
+
}
|
|
2774
|
+
catch (err) {
|
|
2775
|
+
spinner.fail(`Failed: ${err.message}`);
|
|
2776
|
+
console.log(chalk_1.default.yellow("\ncloudflared installs automatically. Check your internet connection or try: npm rebuild cloudflared"));
|
|
2777
|
+
process.exit(1);
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
;
|
|
2568
2781
|
function printTunnelInfo(info) {
|
|
2569
2782
|
console.log("");
|
|
2570
2783
|
console.log(chalk_1.default.cyan(` OpenTunnel ${chalk_1.default.gray(`(via ${info.provider})`)}`));
|
|
@@ -2579,5 +2792,386 @@ function printTunnelInfo(info) {
|
|
|
2579
2792
|
console.log("");
|
|
2580
2793
|
}
|
|
2581
2794
|
;
|
|
2795
|
+
// ============================================================================
|
|
2796
|
+
// Unified Provider Commands
|
|
2797
|
+
// ============================================================================
|
|
2798
|
+
// Login command - unified for all providers
|
|
2799
|
+
program
|
|
2800
|
+
.command("login <provider>")
|
|
2801
|
+
.description("Authenticate with a provider (cloudflare, ngrok)")
|
|
2802
|
+
.option("-t, --token <token>", "Authentication token (required for ngrok)")
|
|
2803
|
+
.action(async (provider, options) => {
|
|
2804
|
+
const credManager = new credentials_1.CredentialsManager();
|
|
2805
|
+
const normalizedProvider = provider.toLowerCase();
|
|
2806
|
+
if (normalizedProvider === "cloudflare" || normalizedProvider === "cf") {
|
|
2807
|
+
const spinner = (0, ora_1.default)("Running cloudflared login...").start();
|
|
2808
|
+
try {
|
|
2809
|
+
const binPath = await CloudflareTunnelClient_1.CloudflareTunnelClient.ensureInstalled();
|
|
2810
|
+
const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
|
|
2811
|
+
spinner.stop();
|
|
2812
|
+
console.log(chalk_1.default.cyan("\n Opening browser for Cloudflare authentication...\n"));
|
|
2813
|
+
const proc = spawn(binPath, ["login"], {
|
|
2814
|
+
stdio: "inherit",
|
|
2815
|
+
shell: process.platform === "win32",
|
|
2816
|
+
});
|
|
2817
|
+
await new Promise((resolve, reject) => {
|
|
2818
|
+
proc.on("close", (code) => {
|
|
2819
|
+
if (code === 0) {
|
|
2820
|
+
const os = require("os");
|
|
2821
|
+
const defaultCertPath = path.join(os.homedir(), ".cloudflared", "cert.pem");
|
|
2822
|
+
credManager.setCloudflare({ certPath: defaultCertPath });
|
|
2823
|
+
console.log(chalk_1.default.green("\n Cloudflare credentials saved"));
|
|
2824
|
+
console.log(chalk_1.default.gray(` Cert path: ${defaultCertPath}`));
|
|
2825
|
+
resolve();
|
|
2826
|
+
}
|
|
2827
|
+
else {
|
|
2828
|
+
reject(new Error(`cloudflared login exited with code ${code}`));
|
|
2829
|
+
}
|
|
2830
|
+
});
|
|
2831
|
+
proc.on("error", reject);
|
|
2832
|
+
});
|
|
2833
|
+
}
|
|
2834
|
+
catch (err) {
|
|
2835
|
+
spinner.fail(`Login failed: ${err.message}`);
|
|
2836
|
+
process.exit(1);
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
else if (normalizedProvider === "ngrok") {
|
|
2840
|
+
if (!options.token) {
|
|
2841
|
+
console.log(chalk_1.default.red("Error: ngrok requires --token <token>"));
|
|
2842
|
+
console.log(chalk_1.default.gray("\nGet your token from: https://dashboard.ngrok.com/get-started/your-authtoken"));
|
|
2843
|
+
console.log(chalk_1.default.cyan("\nUsage: opentunnel login ngrok --token <your-token>"));
|
|
2844
|
+
process.exit(1);
|
|
2845
|
+
}
|
|
2846
|
+
credManager.setNgrok({ token: options.token });
|
|
2847
|
+
console.log(chalk_1.default.green("\n ngrok credentials saved"));
|
|
2848
|
+
console.log(chalk_1.default.gray(` Token: ${options.token.slice(0, 8)}...`));
|
|
2849
|
+
console.log(chalk_1.default.gray(` Stored at: ${credentials_1.CredentialsManager.getCredentialsFile()}\n`));
|
|
2850
|
+
}
|
|
2851
|
+
else {
|
|
2852
|
+
console.log(chalk_1.default.red(`Unknown provider: ${provider}`));
|
|
2853
|
+
console.log(chalk_1.default.gray("\nSupported providers: cloudflare (cf), ngrok"));
|
|
2854
|
+
process.exit(1);
|
|
2855
|
+
}
|
|
2856
|
+
});
|
|
2857
|
+
// Logout command - unified for all providers
|
|
2858
|
+
program
|
|
2859
|
+
.command("logout <provider>")
|
|
2860
|
+
.description("Remove stored credentials for a provider")
|
|
2861
|
+
.action(async (provider) => {
|
|
2862
|
+
const credManager = new credentials_1.CredentialsManager();
|
|
2863
|
+
const normalizedProvider = provider.toLowerCase();
|
|
2864
|
+
if (normalizedProvider === "cloudflare" || normalizedProvider === "cf") {
|
|
2865
|
+
const removed = credManager.removeProvider("cloudflare");
|
|
2866
|
+
if (removed) {
|
|
2867
|
+
console.log(chalk_1.default.green("\n Cloudflare credentials removed\n"));
|
|
2868
|
+
}
|
|
2869
|
+
else {
|
|
2870
|
+
console.log(chalk_1.default.yellow("\n No Cloudflare credentials found\n"));
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
else if (normalizedProvider === "ngrok") {
|
|
2874
|
+
const removed = credManager.removeProvider("ngrok");
|
|
2875
|
+
if (removed) {
|
|
2876
|
+
console.log(chalk_1.default.green("\n ngrok credentials removed\n"));
|
|
2877
|
+
}
|
|
2878
|
+
else {
|
|
2879
|
+
console.log(chalk_1.default.yellow("\n No ngrok credentials found\n"));
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
else {
|
|
2883
|
+
console.log(chalk_1.default.red(`Unknown provider: ${provider}`));
|
|
2884
|
+
console.log(chalk_1.default.gray("\nSupported providers: cloudflare (cf), ngrok"));
|
|
2885
|
+
process.exit(1);
|
|
2886
|
+
}
|
|
2887
|
+
});
|
|
2888
|
+
// Create tunnel command (for named tunnels)
|
|
2889
|
+
program
|
|
2890
|
+
.command("create <name>")
|
|
2891
|
+
.description("Create a named tunnel")
|
|
2892
|
+
.option("--cf, --cloudflare", "Create Cloudflare named tunnel")
|
|
2893
|
+
.option("--provider <provider>", "Provider (cloudflare)")
|
|
2894
|
+
.action(async (name, options) => {
|
|
2895
|
+
const provider = options.provider?.toLowerCase() ||
|
|
2896
|
+
(options.cloudflare || options.cf ? "cloudflare" : null);
|
|
2897
|
+
if (!provider) {
|
|
2898
|
+
console.log(chalk_1.default.red("Error: Provider required"));
|
|
2899
|
+
console.log(chalk_1.default.gray("\nUsage: opentunnel create <name> --cf"));
|
|
2900
|
+
console.log(chalk_1.default.gray(" opentunnel create <name> --provider cloudflare"));
|
|
2901
|
+
process.exit(1);
|
|
2902
|
+
}
|
|
2903
|
+
if (provider === "cloudflare" || provider === "cf") {
|
|
2904
|
+
const spinner = (0, ora_1.default)(`Creating Cloudflare tunnel '${name}'...`).start();
|
|
2905
|
+
try {
|
|
2906
|
+
const binPath = await CloudflareTunnelClient_1.CloudflareTunnelClient.ensureInstalled();
|
|
2907
|
+
const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
|
|
2908
|
+
const proc = spawn(binPath, ["tunnel", "create", name], {
|
|
2909
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2910
|
+
shell: process.platform === "win32",
|
|
2911
|
+
});
|
|
2912
|
+
let output = "";
|
|
2913
|
+
let errorOutput = "";
|
|
2914
|
+
proc.stdout?.on("data", (data) => { output += data.toString(); });
|
|
2915
|
+
proc.stderr?.on("data", (data) => { errorOutput += data.toString(); });
|
|
2916
|
+
await new Promise((resolve, reject) => {
|
|
2917
|
+
proc.on("close", (code) => {
|
|
2918
|
+
if (code === 0) {
|
|
2919
|
+
spinner.succeed(`Tunnel '${name}' created`);
|
|
2920
|
+
if (output)
|
|
2921
|
+
console.log(chalk_1.default.gray(output.trim()));
|
|
2922
|
+
resolve();
|
|
2923
|
+
}
|
|
2924
|
+
else {
|
|
2925
|
+
reject(new Error(errorOutput || `Exit code ${code}`));
|
|
2926
|
+
}
|
|
2927
|
+
});
|
|
2928
|
+
proc.on("error", reject);
|
|
2929
|
+
});
|
|
2930
|
+
}
|
|
2931
|
+
catch (err) {
|
|
2932
|
+
spinner.fail(`Failed to create tunnel: ${err.message}`);
|
|
2933
|
+
if (err.message.includes("login")) {
|
|
2934
|
+
console.log(chalk_1.default.yellow("\nRun 'opentunnel login cloudflare' first"));
|
|
2935
|
+
}
|
|
2936
|
+
process.exit(1);
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
else {
|
|
2940
|
+
console.log(chalk_1.default.red(`Provider '${provider}' doesn't support named tunnels`));
|
|
2941
|
+
process.exit(1);
|
|
2942
|
+
}
|
|
2943
|
+
});
|
|
2944
|
+
// Delete tunnel command
|
|
2945
|
+
program
|
|
2946
|
+
.command("delete <name>")
|
|
2947
|
+
.description("Delete a named tunnel")
|
|
2948
|
+
.option("--cf, --cloudflare", "Delete Cloudflare named tunnel")
|
|
2949
|
+
.option("--provider <provider>", "Provider (cloudflare)")
|
|
2950
|
+
.option("-f, --force", "Force deletion without confirmation")
|
|
2951
|
+
.action(async (name, options) => {
|
|
2952
|
+
const provider = options.provider?.toLowerCase() ||
|
|
2953
|
+
(options.cloudflare || options.cf ? "cloudflare" : null);
|
|
2954
|
+
if (!provider) {
|
|
2955
|
+
console.log(chalk_1.default.red("Error: Provider required"));
|
|
2956
|
+
console.log(chalk_1.default.gray("\nUsage: opentunnel delete <name> --cf"));
|
|
2957
|
+
process.exit(1);
|
|
2958
|
+
}
|
|
2959
|
+
if (provider === "cloudflare" || provider === "cf") {
|
|
2960
|
+
if (!options.force) {
|
|
2961
|
+
console.log(chalk_1.default.yellow(`\n This will delete tunnel '${name}' permanently.`));
|
|
2962
|
+
console.log(chalk_1.default.gray(" Use --force to skip this confirmation.\n"));
|
|
2963
|
+
const readline = await Promise.resolve().then(() => __importStar(require("readline")));
|
|
2964
|
+
const rl = readline.createInterface({
|
|
2965
|
+
input: process.stdin,
|
|
2966
|
+
output: process.stdout,
|
|
2967
|
+
});
|
|
2968
|
+
const answer = await new Promise((resolve) => {
|
|
2969
|
+
rl.question(" Type the tunnel name to confirm: ", resolve);
|
|
2970
|
+
});
|
|
2971
|
+
rl.close();
|
|
2972
|
+
if (answer !== name) {
|
|
2973
|
+
console.log(chalk_1.default.red("\n Cancelled: name didn't match\n"));
|
|
2974
|
+
return;
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
const spinner = (0, ora_1.default)(`Deleting tunnel '${name}'...`).start();
|
|
2978
|
+
try {
|
|
2979
|
+
const binPath = await CloudflareTunnelClient_1.CloudflareTunnelClient.ensureInstalled();
|
|
2980
|
+
const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
|
|
2981
|
+
const proc = spawn(binPath, ["tunnel", "delete", name], {
|
|
2982
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2983
|
+
shell: process.platform === "win32",
|
|
2984
|
+
});
|
|
2985
|
+
let errorOutput = "";
|
|
2986
|
+
proc.stderr?.on("data", (data) => { errorOutput += data.toString(); });
|
|
2987
|
+
await new Promise((resolve, reject) => {
|
|
2988
|
+
proc.on("close", (code) => {
|
|
2989
|
+
if (code === 0) {
|
|
2990
|
+
spinner.succeed(`Tunnel '${name}' deleted`);
|
|
2991
|
+
resolve();
|
|
2992
|
+
}
|
|
2993
|
+
else {
|
|
2994
|
+
reject(new Error(errorOutput || `Exit code ${code}`));
|
|
2995
|
+
}
|
|
2996
|
+
});
|
|
2997
|
+
proc.on("error", reject);
|
|
2998
|
+
});
|
|
2999
|
+
}
|
|
3000
|
+
catch (err) {
|
|
3001
|
+
spinner.fail(`Failed to delete tunnel: ${err.message}`);
|
|
3002
|
+
process.exit(1);
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
});
|
|
3006
|
+
// Route DNS command (Cloudflare specific)
|
|
3007
|
+
program
|
|
3008
|
+
.command("route <name> <hostname>")
|
|
3009
|
+
.description("Route a hostname to a tunnel (Cloudflare DNS)")
|
|
3010
|
+
.option("--cf, --cloudflare", "Route via Cloudflare")
|
|
3011
|
+
.option("--provider <provider>", "Provider (cloudflare)")
|
|
3012
|
+
.action(async (name, hostname, options) => {
|
|
3013
|
+
const provider = options.provider?.toLowerCase() ||
|
|
3014
|
+
(options.cloudflare || options.cf ? "cloudflare" : "cloudflare"); // Default to cloudflare
|
|
3015
|
+
if (provider === "cloudflare" || provider === "cf") {
|
|
3016
|
+
const spinner = (0, ora_1.default)(`Routing ${hostname} to tunnel '${name}'...`).start();
|
|
3017
|
+
try {
|
|
3018
|
+
const binPath = await CloudflareTunnelClient_1.CloudflareTunnelClient.ensureInstalled();
|
|
3019
|
+
const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
|
|
3020
|
+
const proc = spawn(binPath, ["tunnel", "route", "dns", name, hostname], {
|
|
3021
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3022
|
+
shell: process.platform === "win32",
|
|
3023
|
+
});
|
|
3024
|
+
let output = "";
|
|
3025
|
+
let errorOutput = "";
|
|
3026
|
+
proc.stdout?.on("data", (data) => { output += data.toString(); });
|
|
3027
|
+
proc.stderr?.on("data", (data) => { errorOutput += data.toString(); });
|
|
3028
|
+
await new Promise((resolve, reject) => {
|
|
3029
|
+
proc.on("close", (code) => {
|
|
3030
|
+
if (code === 0) {
|
|
3031
|
+
spinner.succeed(`DNS route created: ${hostname} -> ${name}`);
|
|
3032
|
+
if (output)
|
|
3033
|
+
console.log(chalk_1.default.gray(output.trim()));
|
|
3034
|
+
resolve();
|
|
3035
|
+
}
|
|
3036
|
+
else {
|
|
3037
|
+
reject(new Error(errorOutput || `Exit code ${code}`));
|
|
3038
|
+
}
|
|
3039
|
+
});
|
|
3040
|
+
proc.on("error", reject);
|
|
3041
|
+
});
|
|
3042
|
+
}
|
|
3043
|
+
catch (err) {
|
|
3044
|
+
spinner.fail(`Failed to create route: ${err.message}`);
|
|
3045
|
+
process.exit(1);
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
});
|
|
3049
|
+
// List tunnels command - extend existing list command
|
|
3050
|
+
program
|
|
3051
|
+
.command("tunnels")
|
|
3052
|
+
.description("List named tunnels")
|
|
3053
|
+
.option("--cf, --cloudflare", "List Cloudflare tunnels")
|
|
3054
|
+
.option("--provider <provider>", "Provider (cloudflare)")
|
|
3055
|
+
.action(async (options) => {
|
|
3056
|
+
const provider = options.provider?.toLowerCase() ||
|
|
3057
|
+
(options.cloudflare || options.cf ? "cloudflare" : null);
|
|
3058
|
+
if (!provider) {
|
|
3059
|
+
console.log(chalk_1.default.red("Error: Provider required"));
|
|
3060
|
+
console.log(chalk_1.default.gray("\nUsage: opentunnel tunnels --cf"));
|
|
3061
|
+
process.exit(1);
|
|
3062
|
+
}
|
|
3063
|
+
if (provider === "cloudflare" || provider === "cf") {
|
|
3064
|
+
const spinner = (0, ora_1.default)("Listing Cloudflare tunnels...").start();
|
|
3065
|
+
try {
|
|
3066
|
+
const binPath = await CloudflareTunnelClient_1.CloudflareTunnelClient.ensureInstalled();
|
|
3067
|
+
const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
|
|
3068
|
+
const proc = spawn(binPath, ["tunnel", "list"], {
|
|
3069
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3070
|
+
shell: process.platform === "win32",
|
|
3071
|
+
});
|
|
3072
|
+
let output = "";
|
|
3073
|
+
let errorOutput = "";
|
|
3074
|
+
proc.stdout?.on("data", (data) => { output += data.toString(); });
|
|
3075
|
+
proc.stderr?.on("data", (data) => { errorOutput += data.toString(); });
|
|
3076
|
+
await new Promise((resolve, reject) => {
|
|
3077
|
+
proc.on("close", (code) => {
|
|
3078
|
+
if (code === 0) {
|
|
3079
|
+
spinner.stop();
|
|
3080
|
+
console.log(chalk_1.default.cyan("\n Cloudflare Tunnels"));
|
|
3081
|
+
console.log(chalk_1.default.gray(" " + "─".repeat(60)));
|
|
3082
|
+
if (output.trim()) {
|
|
3083
|
+
console.log(output);
|
|
3084
|
+
}
|
|
3085
|
+
else {
|
|
3086
|
+
console.log(chalk_1.default.yellow(" No tunnels found"));
|
|
3087
|
+
console.log(chalk_1.default.gray("\n Create one with: opentunnel create <name> --cf"));
|
|
3088
|
+
}
|
|
3089
|
+
resolve();
|
|
3090
|
+
}
|
|
3091
|
+
else {
|
|
3092
|
+
reject(new Error(errorOutput || `Exit code ${code}`));
|
|
3093
|
+
}
|
|
3094
|
+
});
|
|
3095
|
+
proc.on("error", reject);
|
|
3096
|
+
});
|
|
3097
|
+
}
|
|
3098
|
+
catch (err) {
|
|
3099
|
+
spinner.fail(`Failed to list tunnels: ${err.message}`);
|
|
3100
|
+
if (err.message.includes("login")) {
|
|
3101
|
+
console.log(chalk_1.default.yellow("\nRun 'opentunnel login cloudflare' first"));
|
|
3102
|
+
}
|
|
3103
|
+
process.exit(1);
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
});
|
|
3107
|
+
// ============================================================================
|
|
3108
|
+
// Config Commands
|
|
3109
|
+
// ============================================================================
|
|
3110
|
+
const configCommand = program
|
|
3111
|
+
.command("config")
|
|
3112
|
+
.description("Manage OpenTunnel configuration");
|
|
3113
|
+
configCommand
|
|
3114
|
+
.command("set <key> <value>")
|
|
3115
|
+
.description("Set a configuration value (e.g., ngrok.token, cloudflare.accountId)")
|
|
3116
|
+
.action(async (key, value) => {
|
|
3117
|
+
const credManager = new credentials_1.CredentialsManager();
|
|
3118
|
+
const validKeys = [
|
|
3119
|
+
"ngrok.token",
|
|
3120
|
+
"cloudflare.accountId",
|
|
3121
|
+
"cloudflare.tunnelToken",
|
|
3122
|
+
"cloudflare.certPath",
|
|
3123
|
+
];
|
|
3124
|
+
if (!validKeys.includes(key)) {
|
|
3125
|
+
console.log(chalk_1.default.red(`Invalid key: ${key}`));
|
|
3126
|
+
console.log(chalk_1.default.gray("\nValid keys:"));
|
|
3127
|
+
for (const k of validKeys) {
|
|
3128
|
+
console.log(chalk_1.default.cyan(` ${k}`));
|
|
3129
|
+
}
|
|
3130
|
+
process.exit(1);
|
|
3131
|
+
}
|
|
3132
|
+
credManager.set(key, value);
|
|
3133
|
+
console.log(chalk_1.default.green(`\n ${key} = ${value.slice(0, 20)}${value.length > 20 ? "..." : ""}\n`));
|
|
3134
|
+
});
|
|
3135
|
+
configCommand
|
|
3136
|
+
.command("get <key>")
|
|
3137
|
+
.description("Get a configuration value")
|
|
3138
|
+
.action(async (key) => {
|
|
3139
|
+
const credManager = new credentials_1.CredentialsManager();
|
|
3140
|
+
const value = credManager.get(key);
|
|
3141
|
+
if (value) {
|
|
3142
|
+
// Mask sensitive values
|
|
3143
|
+
const masked = key.includes("token") || key.includes("Token")
|
|
3144
|
+
? value.slice(0, 8) + "..." + value.slice(-4)
|
|
3145
|
+
: value;
|
|
3146
|
+
console.log(chalk_1.default.cyan(`\n ${key} = ${masked}\n`));
|
|
3147
|
+
}
|
|
3148
|
+
else {
|
|
3149
|
+
console.log(chalk_1.default.yellow(`\n ${key} is not set\n`));
|
|
3150
|
+
}
|
|
3151
|
+
});
|
|
3152
|
+
configCommand
|
|
3153
|
+
.command("list")
|
|
3154
|
+
.description("List all stored configuration")
|
|
3155
|
+
.action(async () => {
|
|
3156
|
+
const credManager = new credentials_1.CredentialsManager();
|
|
3157
|
+
const keys = credManager.listKeys();
|
|
3158
|
+
console.log(chalk_1.default.cyan("\n Stored Configuration"));
|
|
3159
|
+
console.log(chalk_1.default.gray(" " + "─".repeat(40)));
|
|
3160
|
+
if (keys.length === 0) {
|
|
3161
|
+
console.log(chalk_1.default.yellow(" No configuration stored"));
|
|
3162
|
+
console.log(chalk_1.default.gray("\n Use 'opentunnel login' or 'opentunnel config set' to add credentials"));
|
|
3163
|
+
}
|
|
3164
|
+
else {
|
|
3165
|
+
for (const key of keys) {
|
|
3166
|
+
const value = credManager.get(key);
|
|
3167
|
+
const masked = (key.includes("token") || key.includes("Token")) && value
|
|
3168
|
+
? value.slice(0, 8) + "..." + value.slice(-4)
|
|
3169
|
+
: value;
|
|
3170
|
+
console.log(` ${chalk_1.default.white(key.padEnd(25))} ${chalk_1.default.gray(masked || "")}`);
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
console.log(chalk_1.default.gray(" " + "─".repeat(40)));
|
|
3174
|
+
console.log(chalk_1.default.gray(`\n Config file: ${credentials_1.CredentialsManager.getCredentialsFile()}\n`));
|
|
3175
|
+
});
|
|
2582
3176
|
program.parse();
|
|
2583
3177
|
//# sourceMappingURL=index.js.map
|