opentunnel-cli 1.0.23 → 1.0.26
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 +43 -2
- package/dist/cli/index.js +722 -85
- 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,10 +42,13 @@ 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"));
|
|
48
50
|
const path = __importStar(require("path"));
|
|
51
|
+
const net = __importStar(require("net"));
|
|
49
52
|
const CONFIG_FILE = "opentunnel.yml";
|
|
50
53
|
function getRegistryPath() {
|
|
51
54
|
const os = require("os");
|
|
@@ -53,6 +56,20 @@ function getRegistryPath() {
|
|
|
53
56
|
const registryDir = path.join(os.homedir(), ".opentunnel");
|
|
54
57
|
return path.join(registryDir, "registry.json");
|
|
55
58
|
}
|
|
59
|
+
function checkPortInUse(port) {
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
const server = net.createServer();
|
|
62
|
+
server.listen(port, () => {
|
|
63
|
+
server.once('close', () => {
|
|
64
|
+
resolve(false);
|
|
65
|
+
});
|
|
66
|
+
server.close();
|
|
67
|
+
});
|
|
68
|
+
server.on('error', () => {
|
|
69
|
+
resolve(true);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
56
73
|
function getLogsDir() {
|
|
57
74
|
const os = require("os");
|
|
58
75
|
const path = require("path");
|
|
@@ -220,8 +237,8 @@ const program = new commander_1.Command();
|
|
|
220
237
|
program
|
|
221
238
|
.name("opentunnel")
|
|
222
239
|
.alias("ot")
|
|
223
|
-
.description("Expose local ports to the internet via custom domains or
|
|
224
|
-
.version("1.0.
|
|
240
|
+
.description("Expose local ports to the internet via custom domains, ngrok, or Cloudflare Tunnel")
|
|
241
|
+
.version("1.0.26");
|
|
225
242
|
// Helper function to build WebSocket URL from domain
|
|
226
243
|
// User only provides base domain (e.g., fjrg2007.com), system handles the rest
|
|
227
244
|
// Note: --insecure flag only affects certificate verification, not the protocol
|
|
@@ -233,6 +250,13 @@ function buildServerUrl(server, basePath) {
|
|
|
233
250
|
hostname = hostname.replace(/\/_tunnel.*$/, "");
|
|
234
251
|
// Remove trailing slash
|
|
235
252
|
hostname = hostname.replace(/\/$/, "");
|
|
253
|
+
// Extract port if present (support both hostname:port and format)
|
|
254
|
+
let port = "";
|
|
255
|
+
const portMatch = hostname.match(/:(\d+)$/);
|
|
256
|
+
if (portMatch) {
|
|
257
|
+
port = `:${portMatch[1]}`;
|
|
258
|
+
hostname = hostname.slice(0, -portMatch[1].length - 1);
|
|
259
|
+
}
|
|
236
260
|
// Build the full hostname with basePath if provided and not empty
|
|
237
261
|
// If basePath is "op" (default), connect to op.domain.com
|
|
238
262
|
// If basePath is empty or not provided, connect directly to domain.com
|
|
@@ -240,7 +264,7 @@ function buildServerUrl(server, basePath) {
|
|
|
240
264
|
const fullHostname = effectiveBasePath ? `${effectiveBasePath}.${hostname}` : hostname;
|
|
241
265
|
// Always use wss:// for remote servers (--insecure only skips cert verification)
|
|
242
266
|
return {
|
|
243
|
-
url: `wss://${fullHostname}/_tunnel`,
|
|
267
|
+
url: `wss://${fullHostname}${port}/_tunnel`,
|
|
244
268
|
displayName: hostname,
|
|
245
269
|
};
|
|
246
270
|
}
|
|
@@ -618,8 +642,28 @@ program
|
|
|
618
642
|
.option("--insecure", "Skip SSL verification (for self-signed certs)")
|
|
619
643
|
.option("--ngrok", "Use ngrok instead of OpenTunnel server")
|
|
620
644
|
.option("--region <region>", "Ngrok region (us, eu, ap, au, sa, jp, in)", "us")
|
|
645
|
+
.option("--cloudflare, --cf", "Use Cloudflare Tunnel instead of OpenTunnel server")
|
|
646
|
+
.option("--cf-hostname <hostname>", "Custom hostname for Cloudflare Tunnel")
|
|
647
|
+
.option("--provider <provider>", "Tunnel provider (opentunnel, ngrok, cloudflare)")
|
|
621
648
|
.action(async (port, options) => {
|
|
622
|
-
|
|
649
|
+
// Determine provider
|
|
650
|
+
const provider = options.provider?.toLowerCase() ||
|
|
651
|
+
(options.cloudflare || options.cf ? "cloudflare" : null) ||
|
|
652
|
+
(options.ngrok ? "ngrok" : null);
|
|
653
|
+
// Cloudflare Tunnel
|
|
654
|
+
if (provider === "cloudflare" || provider === "cf") {
|
|
655
|
+
await createCloudflareTunnel({
|
|
656
|
+
protocol: options.https ? "https" : "http",
|
|
657
|
+
localHost: options.host,
|
|
658
|
+
localPort: parseInt(port),
|
|
659
|
+
hostname: options.cfHostname,
|
|
660
|
+
tunnelName: options.subdomain, // -n works as tunnel name for CF
|
|
661
|
+
noTlsVerify: options.insecure,
|
|
662
|
+
});
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
// ngrok
|
|
666
|
+
if (provider === "ngrok") {
|
|
623
667
|
await createNgrokTunnel({
|
|
624
668
|
protocol: options.https ? "https" : "http",
|
|
625
669
|
localHost: options.host,
|
|
@@ -630,8 +674,8 @@ program
|
|
|
630
674
|
});
|
|
631
675
|
return;
|
|
632
676
|
}
|
|
633
|
-
// If remote server domain provided,
|
|
634
|
-
if (options.domain) {
|
|
677
|
+
// If remote server domain provided, connect to OpenTunnel server
|
|
678
|
+
if (options.domain && options.domain !== "cloudflare" && options.domain !== "ngrok") {
|
|
635
679
|
const { url: serverUrl } = buildServerUrl(options.domain, options.basePath);
|
|
636
680
|
await createTunnel({
|
|
637
681
|
protocol: options.https ? "https" : "http",
|
|
@@ -649,6 +693,8 @@ program
|
|
|
649
693
|
console.log(chalk_1.default.gray("\nExamples:"));
|
|
650
694
|
console.log(chalk_1.default.cyan(" opentunnel http 3000 -s example.com"));
|
|
651
695
|
console.log(chalk_1.default.cyan(" opentunnel http 3000 --domain example.com -n myapp"));
|
|
696
|
+
console.log(chalk_1.default.cyan(" opentunnel http 3000 --cloudflare"));
|
|
697
|
+
console.log(chalk_1.default.cyan(" opentunnel http 3000 --ngrok"));
|
|
652
698
|
process.exit(1);
|
|
653
699
|
});
|
|
654
700
|
// TCP tunnel command
|
|
@@ -664,8 +710,33 @@ program
|
|
|
664
710
|
.option("--insecure", "Skip SSL verification (for self-signed certs)")
|
|
665
711
|
.option("--ngrok", "Use ngrok instead of OpenTunnel server")
|
|
666
712
|
.option("--region <region>", "Ngrok region (us, eu, ap, au, sa, jp, in)", "us")
|
|
713
|
+
.option("--cloudflare, --cf", "Use Cloudflare Tunnel (TCP requires named tunnel)")
|
|
714
|
+
.option("--cf-hostname <hostname>", "Custom hostname for Cloudflare Tunnel")
|
|
715
|
+
.option("--provider <provider>", "Tunnel provider (opentunnel, ngrok, cloudflare)")
|
|
667
716
|
.action(async (port, options) => {
|
|
668
|
-
|
|
717
|
+
// Determine provider
|
|
718
|
+
const provider = options.provider?.toLowerCase() ||
|
|
719
|
+
(options.cloudflare || options.cf ? "cloudflare" : null) ||
|
|
720
|
+
(options.ngrok ? "ngrok" : null);
|
|
721
|
+
// Cloudflare Tunnel - Note: TCP requires named tunnel
|
|
722
|
+
if (provider === "cloudflare" || provider === "cf") {
|
|
723
|
+
if (!options.subdomain) {
|
|
724
|
+
console.log(chalk_1.default.yellow("Note: Cloudflare TCP tunnels require a named tunnel."));
|
|
725
|
+
console.log(chalk_1.default.gray("Use -n <tunnel-name> to specify a named tunnel."));
|
|
726
|
+
console.log(chalk_1.default.gray("\nExample: opentunnel tcp 5432 --cf -n my-tunnel"));
|
|
727
|
+
}
|
|
728
|
+
await createCloudflareTunnel({
|
|
729
|
+
protocol: "tcp",
|
|
730
|
+
localHost: options.host,
|
|
731
|
+
localPort: parseInt(port),
|
|
732
|
+
hostname: options.cfHostname,
|
|
733
|
+
tunnelName: options.subdomain,
|
|
734
|
+
noTlsVerify: options.insecure,
|
|
735
|
+
});
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
// ngrok
|
|
739
|
+
if (provider === "ngrok") {
|
|
669
740
|
await createNgrokTunnel({
|
|
670
741
|
protocol: "tcp",
|
|
671
742
|
localHost: options.host,
|
|
@@ -676,8 +747,8 @@ program
|
|
|
676
747
|
});
|
|
677
748
|
return;
|
|
678
749
|
}
|
|
679
|
-
// If remote server domain provided,
|
|
680
|
-
if (options.domain) {
|
|
750
|
+
// If remote server domain provided, connect to OpenTunnel server
|
|
751
|
+
if (options.domain && options.domain !== "cloudflare" && options.domain !== "ngrok") {
|
|
681
752
|
const { url: serverUrl } = buildServerUrl(options.domain, options.basePath);
|
|
682
753
|
await createTunnel({
|
|
683
754
|
protocol: "tcp",
|
|
@@ -695,6 +766,7 @@ program
|
|
|
695
766
|
console.log(chalk_1.default.gray("\nExamples:"));
|
|
696
767
|
console.log(chalk_1.default.cyan(" opentunnel tcp 5432 -s example.com"));
|
|
697
768
|
console.log(chalk_1.default.cyan(" opentunnel tcp 5432 --domain example.com -r 15432"));
|
|
769
|
+
console.log(chalk_1.default.cyan(" opentunnel tcp 5432 --ngrok"));
|
|
698
770
|
process.exit(1);
|
|
699
771
|
});
|
|
700
772
|
// Quick expose command
|
|
@@ -709,6 +781,7 @@ program
|
|
|
709
781
|
.option("--domain <domain>", "Server domain (e.g., domain.com)")
|
|
710
782
|
.option("--insecure", "Skip SSL certificate verification (for self-signed certs)")
|
|
711
783
|
.option("--ngrok", "Use ngrok instead of OpenTunnel server")
|
|
784
|
+
.option("--cloudflare, --cf", "Use Cloudflare Tunnel instead of OpenTunnel server")
|
|
712
785
|
.action(async (port, options) => {
|
|
713
786
|
const serverUrl = options.server || (options.domain
|
|
714
787
|
? `wss://${options.domain}/_tunnel`
|
|
@@ -717,26 +790,37 @@ program
|
|
|
717
790
|
await runTunnelInBackground("expose", port, { ...options, server: serverUrl });
|
|
718
791
|
return;
|
|
719
792
|
}
|
|
720
|
-
|
|
721
|
-
|
|
793
|
+
// Cloudflare Tunnel
|
|
794
|
+
if (options.cloudflare || options.cf || options.server === "cloudflare") {
|
|
795
|
+
await createCloudflareTunnel({
|
|
722
796
|
protocol: options.protocol,
|
|
723
797
|
localHost: "localhost",
|
|
724
798
|
localPort: parseInt(port),
|
|
725
|
-
|
|
726
|
-
authtoken: options.token
|
|
799
|
+
noTlsVerify: options.insecure,
|
|
727
800
|
});
|
|
801
|
+
return;
|
|
728
802
|
}
|
|
729
|
-
|
|
730
|
-
|
|
803
|
+
// ngrok
|
|
804
|
+
if (options.ngrok || options.server === "ngrok") {
|
|
805
|
+
await createNgrokTunnel({
|
|
731
806
|
protocol: options.protocol,
|
|
732
807
|
localHost: "localhost",
|
|
733
808
|
localPort: parseInt(port),
|
|
734
809
|
subdomain: options.subdomain,
|
|
735
|
-
|
|
736
|
-
token: options.token,
|
|
737
|
-
insecure: options.insecure
|
|
810
|
+
authtoken: options.token
|
|
738
811
|
});
|
|
812
|
+
return;
|
|
739
813
|
}
|
|
814
|
+
// OpenTunnel server
|
|
815
|
+
await createTunnel({
|
|
816
|
+
protocol: options.protocol,
|
|
817
|
+
localHost: "localhost",
|
|
818
|
+
localPort: parseInt(port),
|
|
819
|
+
subdomain: options.subdomain,
|
|
820
|
+
serverUrl,
|
|
821
|
+
token: options.token,
|
|
822
|
+
insecure: options.insecure
|
|
823
|
+
});
|
|
740
824
|
});
|
|
741
825
|
// Server command
|
|
742
826
|
program
|
|
@@ -1613,7 +1697,7 @@ program
|
|
|
1613
1697
|
console.log(chalk_1.default.cyan(`Connecting to ${remote}...\n`));
|
|
1614
1698
|
console.log(chalk_1.default.cyan(`Starting ${tunnelsToStart.length} tunnel(s)...\n`));
|
|
1615
1699
|
try {
|
|
1616
|
-
await startTunnelsFromConfig(tunnelsToStart, serverUrl, config.server?.token, true);
|
|
1700
|
+
await startTunnelsFromConfig(tunnelsToStart, serverUrl, config.server?.token, true, config);
|
|
1617
1701
|
console.log(chalk_1.default.gray("\nPress Ctrl+C to stop"));
|
|
1618
1702
|
// Keep running
|
|
1619
1703
|
await new Promise(() => { });
|
|
@@ -1629,6 +1713,27 @@ program
|
|
|
1629
1713
|
const { TunnelServer } = await Promise.resolve().then(() => __importStar(require("../server/TunnelServer")));
|
|
1630
1714
|
const tcpMin = config.server?.tcpPortMin || 10000;
|
|
1631
1715
|
const tcpMax = config.server?.tcpPortMax || 20000;
|
|
1716
|
+
// Check if port is in use before starting server
|
|
1717
|
+
const isPortInUse = await checkPortInUse(port);
|
|
1718
|
+
if (isPortInUse) {
|
|
1719
|
+
console.log(chalk_1.default.red(`\n❌ Port ${port} is already in use!`));
|
|
1720
|
+
console.log(chalk_1.default.yellow(`Common conflicts:`));
|
|
1721
|
+
if (port === 8080) {
|
|
1722
|
+
console.log(chalk_1.default.gray(` • Pi-hole often uses port 8080`));
|
|
1723
|
+
console.log(chalk_1.default.gray(` • Try: --port 8081 or --port 8443`));
|
|
1724
|
+
}
|
|
1725
|
+
if (port === 443) {
|
|
1726
|
+
console.log(chalk_1.default.gray(` • Web servers (Apache/Nginx) often use port 443`));
|
|
1727
|
+
console.log(chalk_1.default.gray(` • Try: --port 8443 or --port 4430`));
|
|
1728
|
+
}
|
|
1729
|
+
console.log(chalk_1.default.cyan(`\nTo check what's using the port:`));
|
|
1730
|
+
console.log(chalk_1.default.white(` • Linux/macOS: sudo lsof -i :${port}`));
|
|
1731
|
+
console.log(chalk_1.default.white(` • Windows: netstat -ano | findstr :${port}`));
|
|
1732
|
+
console.log(chalk_1.default.cyan(`\nTo use a different port:`));
|
|
1733
|
+
console.log(chalk_1.default.white(` • CLI: opentunnel server --port 8081`));
|
|
1734
|
+
console.log(chalk_1.default.white(` • Config: server.port: 8081`));
|
|
1735
|
+
process.exit(1);
|
|
1736
|
+
}
|
|
1632
1737
|
const spinner = (0, ora_1.default)("Starting server...").start();
|
|
1633
1738
|
const server = new TunnelServer({
|
|
1634
1739
|
port,
|
|
@@ -1665,7 +1770,7 @@ program
|
|
|
1665
1770
|
const serverUrl = `${wsProtocol}://localhost:${port}/_tunnel`;
|
|
1666
1771
|
// Small delay to ensure server is fully ready
|
|
1667
1772
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
1668
|
-
await startTunnelsFromConfig(tunnelsToStart, serverUrl, config.server?.token, true);
|
|
1773
|
+
await startTunnelsFromConfig(tunnelsToStart, serverUrl, config.server?.token, true, config);
|
|
1669
1774
|
}
|
|
1670
1775
|
else if (isServerMode) {
|
|
1671
1776
|
console.log(chalk_1.default.gray("\nServer ready. Waiting for connections..."));
|
|
@@ -2301,79 +2406,175 @@ async function runTunnelInBackgroundFromConfig(name, protocol, port, options) {
|
|
|
2301
2406
|
console.log(chalk_1.default.green(` ✓ ${name}: started (PID: ${child.pid})`));
|
|
2302
2407
|
}
|
|
2303
2408
|
;
|
|
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
|
-
}
|
|
2409
|
+
async function startTunnelsFromConfig(tunnels, serverUrl, token, insecure, globalConfig) {
|
|
2410
|
+
// Determine global provider
|
|
2411
|
+
const globalProvider = globalConfig?.provider || "opentunnel";
|
|
2412
|
+
// Group tunnels by provider
|
|
2413
|
+
const tunnelsByProvider = new Map();
|
|
2414
|
+
for (const tunnel of tunnels) {
|
|
2415
|
+
const provider = tunnel.provider || globalProvider;
|
|
2416
|
+
if (!tunnelsByProvider.has(provider)) {
|
|
2417
|
+
tunnelsByProvider.set(provider, []);
|
|
2333
2418
|
}
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2419
|
+
tunnelsByProvider.get(provider).push(tunnel);
|
|
2420
|
+
}
|
|
2421
|
+
const activeTunnels = [];
|
|
2422
|
+
const clients = [];
|
|
2423
|
+
// Process OpenTunnel tunnels
|
|
2424
|
+
const opentunnelTunnels = tunnelsByProvider.get("opentunnel") || [];
|
|
2425
|
+
if (opentunnelTunnels.length > 0) {
|
|
2426
|
+
const spinner = (0, ora_1.default)("Connecting to OpenTunnel server...").start();
|
|
2427
|
+
const client = new TunnelClient_1.TunnelClient({
|
|
2428
|
+
serverUrl,
|
|
2429
|
+
token,
|
|
2430
|
+
reconnect: true,
|
|
2431
|
+
silent: true,
|
|
2432
|
+
rejectUnauthorized: !insecure,
|
|
2433
|
+
});
|
|
2434
|
+
try {
|
|
2435
|
+
await client.connect();
|
|
2436
|
+
spinner.succeed("Connected to OpenTunnel server");
|
|
2437
|
+
clients.push({ provider: "opentunnel", client });
|
|
2438
|
+
for (const tunnel of opentunnelTunnels) {
|
|
2439
|
+
const tunnelSpinner = (0, ora_1.default)(`Creating tunnel: ${tunnel.name}...`).start();
|
|
2440
|
+
try {
|
|
2441
|
+
const { tunnelId, publicUrl } = await client.createTunnel({
|
|
2442
|
+
protocol: tunnel.protocol,
|
|
2443
|
+
localHost: tunnel.host || "localhost",
|
|
2444
|
+
localPort: tunnel.port,
|
|
2445
|
+
subdomain: tunnel.subdomain,
|
|
2446
|
+
remotePort: tunnel.remotePort,
|
|
2447
|
+
});
|
|
2448
|
+
activeTunnels.push({ name: tunnel.name, tunnelId, publicUrl, provider: "opentunnel", client });
|
|
2449
|
+
tunnelSpinner.succeed(`${tunnel.name}: ${publicUrl}`);
|
|
2450
|
+
}
|
|
2451
|
+
catch (err) {
|
|
2452
|
+
tunnelSpinner.fail(`${tunnel.name}: ${err.message}`);
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
// Handle reconnection events
|
|
2456
|
+
client.on("disconnected", () => {
|
|
2457
|
+
console.log(chalk_1.default.yellow("\n OpenTunnel disconnected, reconnecting..."));
|
|
2458
|
+
});
|
|
2459
|
+
client.on("connected", () => {
|
|
2460
|
+
console.log(chalk_1.default.green(" OpenTunnel reconnected!"));
|
|
2461
|
+
});
|
|
2337
2462
|
}
|
|
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)}`);
|
|
2463
|
+
catch (err) {
|
|
2464
|
+
spinner.fail(`OpenTunnel connection failed: ${err.message}`);
|
|
2343
2465
|
}
|
|
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..."));
|
|
2466
|
+
}
|
|
2467
|
+
// Process ngrok tunnels (one at a time, ngrok limitation)
|
|
2468
|
+
const ngrokTunnels = tunnelsByProvider.get("ngrok") || [];
|
|
2469
|
+
for (const tunnel of ngrokTunnels) {
|
|
2470
|
+
const spinner = (0, ora_1.default)(`Creating ngrok tunnel: ${tunnel.name}...`).start();
|
|
2471
|
+
const ngrokToken = tunnel.ngrokToken || globalConfig?.ngrok?.token;
|
|
2472
|
+
const ngrokRegion = tunnel.ngrokRegion || globalConfig?.ngrok?.region || "us";
|
|
2473
|
+
// Merge IP access config: tunnel-specific > global security > none
|
|
2474
|
+
const { IpFilter } = await Promise.resolve().then(() => __importStar(require("../shared/ip-filter")));
|
|
2475
|
+
const ipAccess = IpFilter.mergeConfigs(globalConfig?.security?.ipAccess, tunnel.ipAccess);
|
|
2476
|
+
const client = new NgrokClient_1.NgrokClient({
|
|
2477
|
+
authtoken: ngrokToken,
|
|
2478
|
+
region: ngrokRegion,
|
|
2479
|
+
ipAccess,
|
|
2368
2480
|
});
|
|
2369
|
-
|
|
2370
|
-
|
|
2481
|
+
try {
|
|
2482
|
+
await client.connect();
|
|
2483
|
+
const { tunnelId, publicUrl } = await client.createTunnel({
|
|
2484
|
+
protocol: tunnel.protocol,
|
|
2485
|
+
localHost: tunnel.host || "localhost",
|
|
2486
|
+
localPort: tunnel.port,
|
|
2487
|
+
subdomain: tunnel.subdomain,
|
|
2488
|
+
remotePort: tunnel.remotePort,
|
|
2489
|
+
});
|
|
2490
|
+
activeTunnels.push({ name: tunnel.name, tunnelId, publicUrl, provider: "ngrok", client });
|
|
2491
|
+
clients.push({ provider: "ngrok", client });
|
|
2492
|
+
spinner.succeed(`${tunnel.name}: ${publicUrl} (ngrok)`);
|
|
2493
|
+
}
|
|
2494
|
+
catch (err) {
|
|
2495
|
+
spinner.fail(`${tunnel.name}: ${err.message}`);
|
|
2496
|
+
console.log(chalk_1.default.yellow(" Make sure ngrok is installed: https://ngrok.com/download"));
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
// Process Cloudflare tunnels (one at a time)
|
|
2500
|
+
const cloudflareTunnels = tunnelsByProvider.get("cloudflare") || [];
|
|
2501
|
+
for (const tunnel of cloudflareTunnels) {
|
|
2502
|
+
const spinner = (0, ora_1.default)(`Creating Cloudflare tunnel: ${tunnel.name}...`).start();
|
|
2503
|
+
const cfHostname = tunnel.cfHostname || globalConfig?.cloudflare?.hostname;
|
|
2504
|
+
// For Cloudflare: subdomain = tunnel name
|
|
2505
|
+
const cfTunnelName = tunnel.subdomain || globalConfig?.cloudflare?.tunnelName;
|
|
2506
|
+
// Merge IP access config: tunnel-specific > global security > none
|
|
2507
|
+
const { IpFilter } = await Promise.resolve().then(() => __importStar(require("../shared/ip-filter")));
|
|
2508
|
+
const ipAccess = IpFilter.mergeConfigs(globalConfig?.security?.ipAccess, tunnel.ipAccess);
|
|
2509
|
+
const client = new CloudflareTunnelClient_1.CloudflareTunnelClient({
|
|
2510
|
+
hostname: cfHostname,
|
|
2511
|
+
tunnelName: cfTunnelName,
|
|
2512
|
+
protocol: tunnel.protocol === "https" ? "https" : "http",
|
|
2513
|
+
ipAccess,
|
|
2371
2514
|
});
|
|
2515
|
+
try {
|
|
2516
|
+
await client.connect();
|
|
2517
|
+
const { tunnelId, publicUrl } = await client.createTunnel({
|
|
2518
|
+
protocol: tunnel.protocol,
|
|
2519
|
+
localHost: tunnel.host || "localhost",
|
|
2520
|
+
localPort: tunnel.port,
|
|
2521
|
+
});
|
|
2522
|
+
activeTunnels.push({ name: tunnel.name, tunnelId, publicUrl, provider: "cloudflare", client });
|
|
2523
|
+
clients.push({ provider: "cloudflare", client });
|
|
2524
|
+
const tunnelMode = cfTunnelName ? ` [${cfTunnelName}]` : "";
|
|
2525
|
+
spinner.succeed(`${tunnel.name}: ${publicUrl}${tunnelMode} (cloudflare)`);
|
|
2526
|
+
}
|
|
2527
|
+
catch (err) {
|
|
2528
|
+
spinner.fail(`${tunnel.name}: ${err.message}`);
|
|
2529
|
+
console.log(chalk_1.default.yellow(" cloudflared installs automatically. Check internet connection or try: npm rebuild cloudflared"));
|
|
2530
|
+
}
|
|
2372
2531
|
}
|
|
2373
|
-
|
|
2374
|
-
|
|
2532
|
+
if (activeTunnels.length === 0) {
|
|
2533
|
+
console.log(chalk_1.default.red("\nNo tunnels created"));
|
|
2375
2534
|
process.exit(1);
|
|
2376
2535
|
}
|
|
2536
|
+
console.log(chalk_1.default.cyan("\n─────────────────────────────────────────"));
|
|
2537
|
+
console.log(chalk_1.default.green(` ${activeTunnels.length} tunnel(s) active`));
|
|
2538
|
+
console.log(chalk_1.default.cyan("─────────────────────────────────────────\n"));
|
|
2539
|
+
for (const t of activeTunnels) {
|
|
2540
|
+
const providerTag = t.provider !== "opentunnel" ? chalk_1.default.gray(` (${t.provider})`) : "";
|
|
2541
|
+
console.log(` ${chalk_1.default.white(t.name.padEnd(15))} ${chalk_1.default.green(t.publicUrl)}${providerTag}`);
|
|
2542
|
+
}
|
|
2543
|
+
console.log(chalk_1.default.gray("\n Press Ctrl+C to stop all tunnels\n"));
|
|
2544
|
+
// Keep alive with uptime counter
|
|
2545
|
+
const startTime = Date.now();
|
|
2546
|
+
const statsInterval = setInterval(() => {
|
|
2547
|
+
const uptime = (0, utils_1.formatDuration)(Date.now() - startTime);
|
|
2548
|
+
process.stdout.write(`\r ${chalk_1.default.gray(`Uptime: ${uptime}`)}`);
|
|
2549
|
+
}, 1000);
|
|
2550
|
+
// Handle exit
|
|
2551
|
+
const cleanup = async () => {
|
|
2552
|
+
clearInterval(statsInterval);
|
|
2553
|
+
console.log("\n");
|
|
2554
|
+
const closeSpinner = (0, ora_1.default)("Closing tunnels...").start();
|
|
2555
|
+
for (const t of activeTunnels) {
|
|
2556
|
+
try {
|
|
2557
|
+
await t.client.closeTunnel(t.tunnelId);
|
|
2558
|
+
}
|
|
2559
|
+
catch {
|
|
2560
|
+
// Ignore errors during cleanup
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
for (const { client } of clients) {
|
|
2564
|
+
try {
|
|
2565
|
+
await client.disconnect();
|
|
2566
|
+
}
|
|
2567
|
+
catch {
|
|
2568
|
+
// Ignore errors during cleanup
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
closeSpinner.succeed("All tunnels closed");
|
|
2572
|
+
process.exit(0);
|
|
2573
|
+
};
|
|
2574
|
+
process.on("SIGINT", cleanup);
|
|
2575
|
+
process.on("SIGTERM", cleanup);
|
|
2576
|
+
// Keep process alive
|
|
2577
|
+
await new Promise(() => { });
|
|
2377
2578
|
}
|
|
2378
2579
|
;
|
|
2379
2580
|
async function runTunnelInBackground(command, port, options) {
|
|
@@ -2565,6 +2766,61 @@ async function createNgrokTunnel(options) {
|
|
|
2565
2766
|
}
|
|
2566
2767
|
}
|
|
2567
2768
|
;
|
|
2769
|
+
async function createCloudflareTunnel(options) {
|
|
2770
|
+
const tunnelType = options.tunnelName ? `named tunnel '${options.tunnelName}'` : "quick tunnel";
|
|
2771
|
+
const spinner = (0, ora_1.default)(`Starting Cloudflare ${tunnelType}...`).start();
|
|
2772
|
+
const client = new CloudflareTunnelClient_1.CloudflareTunnelClient({
|
|
2773
|
+
hostname: options.hostname,
|
|
2774
|
+
tunnelName: options.tunnelName,
|
|
2775
|
+
noTlsVerify: options.noTlsVerify,
|
|
2776
|
+
protocol: options.protocol === "https" ? "https" : "http",
|
|
2777
|
+
});
|
|
2778
|
+
try {
|
|
2779
|
+
await client.connect();
|
|
2780
|
+
spinner.text = "Creating tunnel...";
|
|
2781
|
+
const { tunnelId, publicUrl } = await client.createTunnel({
|
|
2782
|
+
protocol: options.protocol,
|
|
2783
|
+
localHost: options.localHost,
|
|
2784
|
+
localPort: options.localPort,
|
|
2785
|
+
});
|
|
2786
|
+
spinner.succeed("Tunnel established!");
|
|
2787
|
+
const providerInfo = options.tunnelName
|
|
2788
|
+
? `Cloudflare [${options.tunnelName}]`
|
|
2789
|
+
: "Cloudflare";
|
|
2790
|
+
printTunnelInfo({
|
|
2791
|
+
status: "Online",
|
|
2792
|
+
protocol: options.protocol,
|
|
2793
|
+
localHost: options.localHost,
|
|
2794
|
+
localPort: options.localPort,
|
|
2795
|
+
publicUrl,
|
|
2796
|
+
provider: providerInfo,
|
|
2797
|
+
});
|
|
2798
|
+
// Keep alive
|
|
2799
|
+
const startTime = Date.now();
|
|
2800
|
+
const statsInterval = setInterval(() => {
|
|
2801
|
+
const uptime = (0, utils_1.formatDuration)(Date.now() - startTime);
|
|
2802
|
+
process.stdout.write(`\r ${chalk_1.default.gray(`Uptime: ${uptime}`)}`);
|
|
2803
|
+
}, 1000);
|
|
2804
|
+
// Handle exit
|
|
2805
|
+
const cleanup = async () => {
|
|
2806
|
+
clearInterval(statsInterval);
|
|
2807
|
+
console.log("\n");
|
|
2808
|
+
spinner.start("Closing tunnel...");
|
|
2809
|
+
await client.closeTunnel(tunnelId);
|
|
2810
|
+
await client.disconnect();
|
|
2811
|
+
spinner.succeed("Tunnel closed");
|
|
2812
|
+
process.exit(0);
|
|
2813
|
+
};
|
|
2814
|
+
process.on("SIGINT", cleanup);
|
|
2815
|
+
process.on("SIGTERM", cleanup);
|
|
2816
|
+
}
|
|
2817
|
+
catch (err) {
|
|
2818
|
+
spinner.fail(`Failed: ${err.message}`);
|
|
2819
|
+
console.log(chalk_1.default.yellow("\ncloudflared installs automatically. Check your internet connection or try: npm rebuild cloudflared"));
|
|
2820
|
+
process.exit(1);
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
;
|
|
2568
2824
|
function printTunnelInfo(info) {
|
|
2569
2825
|
console.log("");
|
|
2570
2826
|
console.log(chalk_1.default.cyan(` OpenTunnel ${chalk_1.default.gray(`(via ${info.provider})`)}`));
|
|
@@ -2579,5 +2835,386 @@ function printTunnelInfo(info) {
|
|
|
2579
2835
|
console.log("");
|
|
2580
2836
|
}
|
|
2581
2837
|
;
|
|
2838
|
+
// ============================================================================
|
|
2839
|
+
// Unified Provider Commands
|
|
2840
|
+
// ============================================================================
|
|
2841
|
+
// Login command - unified for all providers
|
|
2842
|
+
program
|
|
2843
|
+
.command("login <provider>")
|
|
2844
|
+
.description("Authenticate with a provider (cloudflare, ngrok)")
|
|
2845
|
+
.option("-t, --token <token>", "Authentication token (required for ngrok)")
|
|
2846
|
+
.action(async (provider, options) => {
|
|
2847
|
+
const credManager = new credentials_1.CredentialsManager();
|
|
2848
|
+
const normalizedProvider = provider.toLowerCase();
|
|
2849
|
+
if (normalizedProvider === "cloudflare" || normalizedProvider === "cf") {
|
|
2850
|
+
const spinner = (0, ora_1.default)("Running cloudflared login...").start();
|
|
2851
|
+
try {
|
|
2852
|
+
const binPath = await CloudflareTunnelClient_1.CloudflareTunnelClient.ensureInstalled();
|
|
2853
|
+
const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
|
|
2854
|
+
spinner.stop();
|
|
2855
|
+
console.log(chalk_1.default.cyan("\n Opening browser for Cloudflare authentication...\n"));
|
|
2856
|
+
const proc = spawn(binPath, ["login"], {
|
|
2857
|
+
stdio: "inherit",
|
|
2858
|
+
shell: process.platform === "win32",
|
|
2859
|
+
});
|
|
2860
|
+
await new Promise((resolve, reject) => {
|
|
2861
|
+
proc.on("close", (code) => {
|
|
2862
|
+
if (code === 0) {
|
|
2863
|
+
const os = require("os");
|
|
2864
|
+
const defaultCertPath = path.join(os.homedir(), ".cloudflared", "cert.pem");
|
|
2865
|
+
credManager.setCloudflare({ certPath: defaultCertPath });
|
|
2866
|
+
console.log(chalk_1.default.green("\n Cloudflare credentials saved"));
|
|
2867
|
+
console.log(chalk_1.default.gray(` Cert path: ${defaultCertPath}`));
|
|
2868
|
+
resolve();
|
|
2869
|
+
}
|
|
2870
|
+
else {
|
|
2871
|
+
reject(new Error(`cloudflared login exited with code ${code}`));
|
|
2872
|
+
}
|
|
2873
|
+
});
|
|
2874
|
+
proc.on("error", reject);
|
|
2875
|
+
});
|
|
2876
|
+
}
|
|
2877
|
+
catch (err) {
|
|
2878
|
+
spinner.fail(`Login failed: ${err.message}`);
|
|
2879
|
+
process.exit(1);
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
else if (normalizedProvider === "ngrok") {
|
|
2883
|
+
if (!options.token) {
|
|
2884
|
+
console.log(chalk_1.default.red("Error: ngrok requires --token <token>"));
|
|
2885
|
+
console.log(chalk_1.default.gray("\nGet your token from: https://dashboard.ngrok.com/get-started/your-authtoken"));
|
|
2886
|
+
console.log(chalk_1.default.cyan("\nUsage: opentunnel login ngrok --token <your-token>"));
|
|
2887
|
+
process.exit(1);
|
|
2888
|
+
}
|
|
2889
|
+
credManager.setNgrok({ token: options.token });
|
|
2890
|
+
console.log(chalk_1.default.green("\n ngrok credentials saved"));
|
|
2891
|
+
console.log(chalk_1.default.gray(` Token: ${options.token.slice(0, 8)}...`));
|
|
2892
|
+
console.log(chalk_1.default.gray(` Stored at: ${credentials_1.CredentialsManager.getCredentialsFile()}\n`));
|
|
2893
|
+
}
|
|
2894
|
+
else {
|
|
2895
|
+
console.log(chalk_1.default.red(`Unknown provider: ${provider}`));
|
|
2896
|
+
console.log(chalk_1.default.gray("\nSupported providers: cloudflare (cf), ngrok"));
|
|
2897
|
+
process.exit(1);
|
|
2898
|
+
}
|
|
2899
|
+
});
|
|
2900
|
+
// Logout command - unified for all providers
|
|
2901
|
+
program
|
|
2902
|
+
.command("logout <provider>")
|
|
2903
|
+
.description("Remove stored credentials for a provider")
|
|
2904
|
+
.action(async (provider) => {
|
|
2905
|
+
const credManager = new credentials_1.CredentialsManager();
|
|
2906
|
+
const normalizedProvider = provider.toLowerCase();
|
|
2907
|
+
if (normalizedProvider === "cloudflare" || normalizedProvider === "cf") {
|
|
2908
|
+
const removed = credManager.removeProvider("cloudflare");
|
|
2909
|
+
if (removed) {
|
|
2910
|
+
console.log(chalk_1.default.green("\n Cloudflare credentials removed\n"));
|
|
2911
|
+
}
|
|
2912
|
+
else {
|
|
2913
|
+
console.log(chalk_1.default.yellow("\n No Cloudflare credentials found\n"));
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
else if (normalizedProvider === "ngrok") {
|
|
2917
|
+
const removed = credManager.removeProvider("ngrok");
|
|
2918
|
+
if (removed) {
|
|
2919
|
+
console.log(chalk_1.default.green("\n ngrok credentials removed\n"));
|
|
2920
|
+
}
|
|
2921
|
+
else {
|
|
2922
|
+
console.log(chalk_1.default.yellow("\n No ngrok credentials found\n"));
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
else {
|
|
2926
|
+
console.log(chalk_1.default.red(`Unknown provider: ${provider}`));
|
|
2927
|
+
console.log(chalk_1.default.gray("\nSupported providers: cloudflare (cf), ngrok"));
|
|
2928
|
+
process.exit(1);
|
|
2929
|
+
}
|
|
2930
|
+
});
|
|
2931
|
+
// Create tunnel command (for named tunnels)
|
|
2932
|
+
program
|
|
2933
|
+
.command("create <name>")
|
|
2934
|
+
.description("Create a named tunnel")
|
|
2935
|
+
.option("--cf, --cloudflare", "Create Cloudflare named tunnel")
|
|
2936
|
+
.option("--provider <provider>", "Provider (cloudflare)")
|
|
2937
|
+
.action(async (name, options) => {
|
|
2938
|
+
const provider = options.provider?.toLowerCase() ||
|
|
2939
|
+
(options.cloudflare || options.cf ? "cloudflare" : null);
|
|
2940
|
+
if (!provider) {
|
|
2941
|
+
console.log(chalk_1.default.red("Error: Provider required"));
|
|
2942
|
+
console.log(chalk_1.default.gray("\nUsage: opentunnel create <name> --cf"));
|
|
2943
|
+
console.log(chalk_1.default.gray(" opentunnel create <name> --provider cloudflare"));
|
|
2944
|
+
process.exit(1);
|
|
2945
|
+
}
|
|
2946
|
+
if (provider === "cloudflare" || provider === "cf") {
|
|
2947
|
+
const spinner = (0, ora_1.default)(`Creating Cloudflare tunnel '${name}'...`).start();
|
|
2948
|
+
try {
|
|
2949
|
+
const binPath = await CloudflareTunnelClient_1.CloudflareTunnelClient.ensureInstalled();
|
|
2950
|
+
const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
|
|
2951
|
+
const proc = spawn(binPath, ["tunnel", "create", name], {
|
|
2952
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2953
|
+
shell: process.platform === "win32",
|
|
2954
|
+
});
|
|
2955
|
+
let output = "";
|
|
2956
|
+
let errorOutput = "";
|
|
2957
|
+
proc.stdout?.on("data", (data) => { output += data.toString(); });
|
|
2958
|
+
proc.stderr?.on("data", (data) => { errorOutput += data.toString(); });
|
|
2959
|
+
await new Promise((resolve, reject) => {
|
|
2960
|
+
proc.on("close", (code) => {
|
|
2961
|
+
if (code === 0) {
|
|
2962
|
+
spinner.succeed(`Tunnel '${name}' created`);
|
|
2963
|
+
if (output)
|
|
2964
|
+
console.log(chalk_1.default.gray(output.trim()));
|
|
2965
|
+
resolve();
|
|
2966
|
+
}
|
|
2967
|
+
else {
|
|
2968
|
+
reject(new Error(errorOutput || `Exit code ${code}`));
|
|
2969
|
+
}
|
|
2970
|
+
});
|
|
2971
|
+
proc.on("error", reject);
|
|
2972
|
+
});
|
|
2973
|
+
}
|
|
2974
|
+
catch (err) {
|
|
2975
|
+
spinner.fail(`Failed to create tunnel: ${err.message}`);
|
|
2976
|
+
if (err.message.includes("login")) {
|
|
2977
|
+
console.log(chalk_1.default.yellow("\nRun 'opentunnel login cloudflare' first"));
|
|
2978
|
+
}
|
|
2979
|
+
process.exit(1);
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
else {
|
|
2983
|
+
console.log(chalk_1.default.red(`Provider '${provider}' doesn't support named tunnels`));
|
|
2984
|
+
process.exit(1);
|
|
2985
|
+
}
|
|
2986
|
+
});
|
|
2987
|
+
// Delete tunnel command
|
|
2988
|
+
program
|
|
2989
|
+
.command("delete <name>")
|
|
2990
|
+
.description("Delete a named tunnel")
|
|
2991
|
+
.option("--cf, --cloudflare", "Delete Cloudflare named tunnel")
|
|
2992
|
+
.option("--provider <provider>", "Provider (cloudflare)")
|
|
2993
|
+
.option("-f, --force", "Force deletion without confirmation")
|
|
2994
|
+
.action(async (name, options) => {
|
|
2995
|
+
const provider = options.provider?.toLowerCase() ||
|
|
2996
|
+
(options.cloudflare || options.cf ? "cloudflare" : null);
|
|
2997
|
+
if (!provider) {
|
|
2998
|
+
console.log(chalk_1.default.red("Error: Provider required"));
|
|
2999
|
+
console.log(chalk_1.default.gray("\nUsage: opentunnel delete <name> --cf"));
|
|
3000
|
+
process.exit(1);
|
|
3001
|
+
}
|
|
3002
|
+
if (provider === "cloudflare" || provider === "cf") {
|
|
3003
|
+
if (!options.force) {
|
|
3004
|
+
console.log(chalk_1.default.yellow(`\n This will delete tunnel '${name}' permanently.`));
|
|
3005
|
+
console.log(chalk_1.default.gray(" Use --force to skip this confirmation.\n"));
|
|
3006
|
+
const readline = await Promise.resolve().then(() => __importStar(require("readline")));
|
|
3007
|
+
const rl = readline.createInterface({
|
|
3008
|
+
input: process.stdin,
|
|
3009
|
+
output: process.stdout,
|
|
3010
|
+
});
|
|
3011
|
+
const answer = await new Promise((resolve) => {
|
|
3012
|
+
rl.question(" Type the tunnel name to confirm: ", resolve);
|
|
3013
|
+
});
|
|
3014
|
+
rl.close();
|
|
3015
|
+
if (answer !== name) {
|
|
3016
|
+
console.log(chalk_1.default.red("\n Cancelled: name didn't match\n"));
|
|
3017
|
+
return;
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
const spinner = (0, ora_1.default)(`Deleting tunnel '${name}'...`).start();
|
|
3021
|
+
try {
|
|
3022
|
+
const binPath = await CloudflareTunnelClient_1.CloudflareTunnelClient.ensureInstalled();
|
|
3023
|
+
const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
|
|
3024
|
+
const proc = spawn(binPath, ["tunnel", "delete", name], {
|
|
3025
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3026
|
+
shell: process.platform === "win32",
|
|
3027
|
+
});
|
|
3028
|
+
let errorOutput = "";
|
|
3029
|
+
proc.stderr?.on("data", (data) => { errorOutput += data.toString(); });
|
|
3030
|
+
await new Promise((resolve, reject) => {
|
|
3031
|
+
proc.on("close", (code) => {
|
|
3032
|
+
if (code === 0) {
|
|
3033
|
+
spinner.succeed(`Tunnel '${name}' deleted`);
|
|
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 delete tunnel: ${err.message}`);
|
|
3045
|
+
process.exit(1);
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
});
|
|
3049
|
+
// Route DNS command (Cloudflare specific)
|
|
3050
|
+
program
|
|
3051
|
+
.command("route <name> <hostname>")
|
|
3052
|
+
.description("Route a hostname to a tunnel (Cloudflare DNS)")
|
|
3053
|
+
.option("--cf, --cloudflare", "Route via Cloudflare")
|
|
3054
|
+
.option("--provider <provider>", "Provider (cloudflare)")
|
|
3055
|
+
.action(async (name, hostname, options) => {
|
|
3056
|
+
const provider = options.provider?.toLowerCase() ||
|
|
3057
|
+
(options.cloudflare || options.cf ? "cloudflare" : "cloudflare"); // Default to cloudflare
|
|
3058
|
+
if (provider === "cloudflare" || provider === "cf") {
|
|
3059
|
+
const spinner = (0, ora_1.default)(`Routing ${hostname} to tunnel '${name}'...`).start();
|
|
3060
|
+
try {
|
|
3061
|
+
const binPath = await CloudflareTunnelClient_1.CloudflareTunnelClient.ensureInstalled();
|
|
3062
|
+
const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
|
|
3063
|
+
const proc = spawn(binPath, ["tunnel", "route", "dns", name, hostname], {
|
|
3064
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3065
|
+
shell: process.platform === "win32",
|
|
3066
|
+
});
|
|
3067
|
+
let output = "";
|
|
3068
|
+
let errorOutput = "";
|
|
3069
|
+
proc.stdout?.on("data", (data) => { output += data.toString(); });
|
|
3070
|
+
proc.stderr?.on("data", (data) => { errorOutput += data.toString(); });
|
|
3071
|
+
await new Promise((resolve, reject) => {
|
|
3072
|
+
proc.on("close", (code) => {
|
|
3073
|
+
if (code === 0) {
|
|
3074
|
+
spinner.succeed(`DNS route created: ${hostname} -> ${name}`);
|
|
3075
|
+
if (output)
|
|
3076
|
+
console.log(chalk_1.default.gray(output.trim()));
|
|
3077
|
+
resolve();
|
|
3078
|
+
}
|
|
3079
|
+
else {
|
|
3080
|
+
reject(new Error(errorOutput || `Exit code ${code}`));
|
|
3081
|
+
}
|
|
3082
|
+
});
|
|
3083
|
+
proc.on("error", reject);
|
|
3084
|
+
});
|
|
3085
|
+
}
|
|
3086
|
+
catch (err) {
|
|
3087
|
+
spinner.fail(`Failed to create route: ${err.message}`);
|
|
3088
|
+
process.exit(1);
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
});
|
|
3092
|
+
// List tunnels command - extend existing list command
|
|
3093
|
+
program
|
|
3094
|
+
.command("tunnels")
|
|
3095
|
+
.description("List named tunnels")
|
|
3096
|
+
.option("--cf, --cloudflare", "List Cloudflare tunnels")
|
|
3097
|
+
.option("--provider <provider>", "Provider (cloudflare)")
|
|
3098
|
+
.action(async (options) => {
|
|
3099
|
+
const provider = options.provider?.toLowerCase() ||
|
|
3100
|
+
(options.cloudflare || options.cf ? "cloudflare" : null);
|
|
3101
|
+
if (!provider) {
|
|
3102
|
+
console.log(chalk_1.default.red("Error: Provider required"));
|
|
3103
|
+
console.log(chalk_1.default.gray("\nUsage: opentunnel tunnels --cf"));
|
|
3104
|
+
process.exit(1);
|
|
3105
|
+
}
|
|
3106
|
+
if (provider === "cloudflare" || provider === "cf") {
|
|
3107
|
+
const spinner = (0, ora_1.default)("Listing Cloudflare tunnels...").start();
|
|
3108
|
+
try {
|
|
3109
|
+
const binPath = await CloudflareTunnelClient_1.CloudflareTunnelClient.ensureInstalled();
|
|
3110
|
+
const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
|
|
3111
|
+
const proc = spawn(binPath, ["tunnel", "list"], {
|
|
3112
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3113
|
+
shell: process.platform === "win32",
|
|
3114
|
+
});
|
|
3115
|
+
let output = "";
|
|
3116
|
+
let errorOutput = "";
|
|
3117
|
+
proc.stdout?.on("data", (data) => { output += data.toString(); });
|
|
3118
|
+
proc.stderr?.on("data", (data) => { errorOutput += data.toString(); });
|
|
3119
|
+
await new Promise((resolve, reject) => {
|
|
3120
|
+
proc.on("close", (code) => {
|
|
3121
|
+
if (code === 0) {
|
|
3122
|
+
spinner.stop();
|
|
3123
|
+
console.log(chalk_1.default.cyan("\n Cloudflare Tunnels"));
|
|
3124
|
+
console.log(chalk_1.default.gray(" " + "─".repeat(60)));
|
|
3125
|
+
if (output.trim()) {
|
|
3126
|
+
console.log(output);
|
|
3127
|
+
}
|
|
3128
|
+
else {
|
|
3129
|
+
console.log(chalk_1.default.yellow(" No tunnels found"));
|
|
3130
|
+
console.log(chalk_1.default.gray("\n Create one with: opentunnel create <name> --cf"));
|
|
3131
|
+
}
|
|
3132
|
+
resolve();
|
|
3133
|
+
}
|
|
3134
|
+
else {
|
|
3135
|
+
reject(new Error(errorOutput || `Exit code ${code}`));
|
|
3136
|
+
}
|
|
3137
|
+
});
|
|
3138
|
+
proc.on("error", reject);
|
|
3139
|
+
});
|
|
3140
|
+
}
|
|
3141
|
+
catch (err) {
|
|
3142
|
+
spinner.fail(`Failed to list tunnels: ${err.message}`);
|
|
3143
|
+
if (err.message.includes("login")) {
|
|
3144
|
+
console.log(chalk_1.default.yellow("\nRun 'opentunnel login cloudflare' first"));
|
|
3145
|
+
}
|
|
3146
|
+
process.exit(1);
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
});
|
|
3150
|
+
// ============================================================================
|
|
3151
|
+
// Config Commands
|
|
3152
|
+
// ============================================================================
|
|
3153
|
+
const configCommand = program
|
|
3154
|
+
.command("config")
|
|
3155
|
+
.description("Manage OpenTunnel configuration");
|
|
3156
|
+
configCommand
|
|
3157
|
+
.command("set <key> <value>")
|
|
3158
|
+
.description("Set a configuration value (e.g., ngrok.token, cloudflare.accountId)")
|
|
3159
|
+
.action(async (key, value) => {
|
|
3160
|
+
const credManager = new credentials_1.CredentialsManager();
|
|
3161
|
+
const validKeys = [
|
|
3162
|
+
"ngrok.token",
|
|
3163
|
+
"cloudflare.accountId",
|
|
3164
|
+
"cloudflare.tunnelToken",
|
|
3165
|
+
"cloudflare.certPath",
|
|
3166
|
+
];
|
|
3167
|
+
if (!validKeys.includes(key)) {
|
|
3168
|
+
console.log(chalk_1.default.red(`Invalid key: ${key}`));
|
|
3169
|
+
console.log(chalk_1.default.gray("\nValid keys:"));
|
|
3170
|
+
for (const k of validKeys) {
|
|
3171
|
+
console.log(chalk_1.default.cyan(` ${k}`));
|
|
3172
|
+
}
|
|
3173
|
+
process.exit(1);
|
|
3174
|
+
}
|
|
3175
|
+
credManager.set(key, value);
|
|
3176
|
+
console.log(chalk_1.default.green(`\n ${key} = ${value.slice(0, 20)}${value.length > 20 ? "..." : ""}\n`));
|
|
3177
|
+
});
|
|
3178
|
+
configCommand
|
|
3179
|
+
.command("get <key>")
|
|
3180
|
+
.description("Get a configuration value")
|
|
3181
|
+
.action(async (key) => {
|
|
3182
|
+
const credManager = new credentials_1.CredentialsManager();
|
|
3183
|
+
const value = credManager.get(key);
|
|
3184
|
+
if (value) {
|
|
3185
|
+
// Mask sensitive values
|
|
3186
|
+
const masked = key.includes("token") || key.includes("Token")
|
|
3187
|
+
? value.slice(0, 8) + "..." + value.slice(-4)
|
|
3188
|
+
: value;
|
|
3189
|
+
console.log(chalk_1.default.cyan(`\n ${key} = ${masked}\n`));
|
|
3190
|
+
}
|
|
3191
|
+
else {
|
|
3192
|
+
console.log(chalk_1.default.yellow(`\n ${key} is not set\n`));
|
|
3193
|
+
}
|
|
3194
|
+
});
|
|
3195
|
+
configCommand
|
|
3196
|
+
.command("list")
|
|
3197
|
+
.description("List all stored configuration")
|
|
3198
|
+
.action(async () => {
|
|
3199
|
+
const credManager = new credentials_1.CredentialsManager();
|
|
3200
|
+
const keys = credManager.listKeys();
|
|
3201
|
+
console.log(chalk_1.default.cyan("\n Stored Configuration"));
|
|
3202
|
+
console.log(chalk_1.default.gray(" " + "─".repeat(40)));
|
|
3203
|
+
if (keys.length === 0) {
|
|
3204
|
+
console.log(chalk_1.default.yellow(" No configuration stored"));
|
|
3205
|
+
console.log(chalk_1.default.gray("\n Use 'opentunnel login' or 'opentunnel config set' to add credentials"));
|
|
3206
|
+
}
|
|
3207
|
+
else {
|
|
3208
|
+
for (const key of keys) {
|
|
3209
|
+
const value = credManager.get(key);
|
|
3210
|
+
const masked = (key.includes("token") || key.includes("Token")) && value
|
|
3211
|
+
? value.slice(0, 8) + "..." + value.slice(-4)
|
|
3212
|
+
: value;
|
|
3213
|
+
console.log(` ${chalk_1.default.white(key.padEnd(25))} ${chalk_1.default.gray(masked || "")}`);
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3216
|
+
console.log(chalk_1.default.gray(" " + "─".repeat(40)));
|
|
3217
|
+
console.log(chalk_1.default.gray(`\n Config file: ${credentials_1.CredentialsManager.getCredentialsFile()}\n`));
|
|
3218
|
+
});
|
|
2582
3219
|
program.parse();
|
|
2583
3220
|
//# sourceMappingURL=index.js.map
|