opentunnel-cli 1.0.28 → 1.0.30
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 +2 -1
- package/dist/cli/index.js +60 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/client/CloudflareTunnelClient.d.ts +7 -1
- package/dist/client/CloudflareTunnelClient.d.ts.map +1 -1
- package/dist/client/CloudflareTunnelClient.js +44 -4
- package/dist/client/CloudflareTunnelClient.js.map +1 -1
- package/dist/server/TunnelServer.d.ts.map +1 -1
- package/dist/server/TunnelServer.js +23 -1
- package/dist/server/TunnelServer.js.map +1 -1
- package/dist/shared/types.d.ts +1 -0
- package/dist/shared/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -208,10 +208,11 @@ tunnels:
|
|
|
208
208
|
**Server options:**
|
|
209
209
|
- `-d, --detach` - Run in background
|
|
210
210
|
- `--domain <domain>` - Server domain
|
|
211
|
-
- `--port <port>` - Listen port (default:
|
|
211
|
+
- `--port <port>` - Listen port (default: 443)
|
|
212
212
|
- `--auth-tokens <tokens>` - Comma-separated auth tokens
|
|
213
213
|
- `--letsencrypt` - Enable Let's Encrypt SSL
|
|
214
214
|
- `--cloudflare-token` - Cloudflare API token for DNS
|
|
215
|
+
- `--no-http-redirect` - Disable automatic HTTP→HTTPS redirect on port 80
|
|
215
216
|
|
|
216
217
|
**Client options:**
|
|
217
218
|
- `-s, --server <url>` - Server URL
|
package/dist/cli/index.js
CHANGED
|
@@ -238,7 +238,7 @@ program
|
|
|
238
238
|
.name("opentunnel")
|
|
239
239
|
.alias("ot")
|
|
240
240
|
.description("Expose local ports to the internet via custom domains, ngrok, or Cloudflare Tunnel")
|
|
241
|
-
.version("1.0.
|
|
241
|
+
.version("1.0.30");
|
|
242
242
|
// Helper function to build WebSocket URL from domain
|
|
243
243
|
// User only provides base domain (e.g., fjrg2007.com), system handles the rest
|
|
244
244
|
// Note: --insecure flag only affects certificate verification, not the protocol
|
|
@@ -316,11 +316,12 @@ program
|
|
|
316
316
|
basePath: basePath || "op",
|
|
317
317
|
tunnelPortRange: { min: 10000, max: 20000 },
|
|
318
318
|
selfSignedHttps: { enabled: true },
|
|
319
|
+
httpRedirect: true,
|
|
319
320
|
auth: options.token ? { required: true, tokens: [options.token] } : undefined,
|
|
320
321
|
});
|
|
321
322
|
try {
|
|
322
323
|
await localServer.start();
|
|
323
|
-
console.log(chalk_1.default.green(
|
|
324
|
+
console.log(chalk_1.default.green(`Server running on port ${serverPort}\n`));
|
|
324
325
|
}
|
|
325
326
|
catch (err) {
|
|
326
327
|
console.log(chalk_1.default.red(`Failed to start server: ${err.message}`));
|
|
@@ -550,6 +551,7 @@ program
|
|
|
550
551
|
basePath: basePath || "op",
|
|
551
552
|
tunnelPortRange: { min: 10000, max: 20000 },
|
|
552
553
|
selfSignedHttps: { enabled: true },
|
|
554
|
+
httpRedirect: true,
|
|
553
555
|
auth: options.token ? { required: true, tokens: [options.token] } : undefined,
|
|
554
556
|
});
|
|
555
557
|
await localServer.start();
|
|
@@ -835,6 +837,7 @@ program
|
|
|
835
837
|
.option("--tcp-max <port>", "Maximum TCP port")
|
|
836
838
|
.option("--auth-tokens <tokens>", "Comma-separated auth tokens")
|
|
837
839
|
.option("--no-https", "Disable HTTPS (use plain HTTP)")
|
|
840
|
+
.option("--no-http-redirect", "Disable HTTP→HTTPS redirect on port 80 (enabled by default)")
|
|
838
841
|
.option("--https-cert <path>", "Path to SSL certificate (for custom certs)")
|
|
839
842
|
.option("--https-key <path>", "Path to SSL private key (for custom certs)")
|
|
840
843
|
.option("--letsencrypt", "Use Let's Encrypt instead of self-signed (requires port 80)")
|
|
@@ -891,6 +894,7 @@ program
|
|
|
891
894
|
dymoBlockHosting: options.dymoBlockHosting ?? fileConfig.dymo?.blockHosting ?? false,
|
|
892
895
|
dymoCache: options.dymoCache ?? fileConfig.dymo?.cacheResults ?? true,
|
|
893
896
|
dymoCacheTtl: options.dymoCacheTtl ? parseInt(options.dymoCacheTtl) : (fileConfig.dymo?.cacheTTL ?? 300),
|
|
897
|
+
httpRedirect: options.httpRedirect !== false,
|
|
894
898
|
detach: options.detach,
|
|
895
899
|
};
|
|
896
900
|
// Detached mode - run in background
|
|
@@ -997,6 +1001,8 @@ program
|
|
|
997
1001
|
args.push("--dymo-block-proxies");
|
|
998
1002
|
if (mergedOptions.dymoBlockHosting)
|
|
999
1003
|
args.push("--dymo-block-hosting");
|
|
1004
|
+
if (mergedOptions.httpRedirect === false)
|
|
1005
|
+
args.push("--no-http-redirect");
|
|
1000
1006
|
const out = fsAsync.openSync(logFile, "a");
|
|
1001
1007
|
const err = fsAsync.openSync(logFile, "a");
|
|
1002
1008
|
const child = spawn(process.execPath, [process.argv[1], ...args], {
|
|
@@ -1127,6 +1133,7 @@ program
|
|
|
1127
1133
|
https: httpsConfig,
|
|
1128
1134
|
selfSignedHttps: selfSignedHttpsConfig,
|
|
1129
1135
|
autoHttps: autoHttpsConfig,
|
|
1136
|
+
httpRedirect: mergedOptions.httpRedirect,
|
|
1130
1137
|
autoDns: detectDnsConfig(mergedOptions)
|
|
1131
1138
|
});
|
|
1132
1139
|
// Helper function to auto-detect DNS provider
|
|
@@ -1802,7 +1809,15 @@ program
|
|
|
1802
1809
|
const isClientMode = mode === "client";
|
|
1803
1810
|
const isServerMode = mode === "server";
|
|
1804
1811
|
const isHybridMode = mode === "hybrid";
|
|
1805
|
-
if (
|
|
1812
|
+
// Check if using external provider (cloudflare/ngrok) which doesn't need domain/remote
|
|
1813
|
+
const globalProvider = config.provider || "opentunnel";
|
|
1814
|
+
const isExternalProvider = globalProvider === "cloudflare" || globalProvider === "ngrok";
|
|
1815
|
+
// Also check if all tunnels use external providers
|
|
1816
|
+
const allTunnelsExternal = hasTunnels && tunnelsToStart.every(t => {
|
|
1817
|
+
const provider = t.provider || globalProvider;
|
|
1818
|
+
return provider === "cloudflare" || provider === "ngrok";
|
|
1819
|
+
});
|
|
1820
|
+
if (!hasDomainConfig && !remote && !isExternalProvider && !allTunnelsExternal) {
|
|
1806
1821
|
console.log(chalk_1.default.red("Missing configuration."));
|
|
1807
1822
|
console.log(chalk_1.default.gray("\nAdd to your config:"));
|
|
1808
1823
|
console.log(chalk_1.default.cyan("\n # Run your own server:"));
|
|
@@ -1811,8 +1826,33 @@ program
|
|
|
1811
1826
|
console.log(chalk_1.default.cyan("\n # Or connect to a remote server:"));
|
|
1812
1827
|
console.log(chalk_1.default.white(" server:"));
|
|
1813
1828
|
console.log(chalk_1.default.white(" remote: example.com"));
|
|
1829
|
+
console.log(chalk_1.default.cyan("\n # Or use an external provider:"));
|
|
1830
|
+
console.log(chalk_1.default.white(" provider: cloudflare # or ngrok"));
|
|
1814
1831
|
process.exit(1);
|
|
1815
1832
|
}
|
|
1833
|
+
// External provider mode: skip OpenTunnel server, use cloudflare/ngrok directly
|
|
1834
|
+
if ((isExternalProvider || allTunnelsExternal) && !hasDomainConfig && !remote) {
|
|
1835
|
+
if (!hasTunnels) {
|
|
1836
|
+
console.log(chalk_1.default.red("No tunnels configured."));
|
|
1837
|
+
console.log(chalk_1.default.gray("\nAdd tunnels to your config:"));
|
|
1838
|
+
console.log(chalk_1.default.white(" tunnels:"));
|
|
1839
|
+
console.log(chalk_1.default.white(" - name: web"));
|
|
1840
|
+
console.log(chalk_1.default.white(" protocol: http"));
|
|
1841
|
+
console.log(chalk_1.default.white(" port: 80"));
|
|
1842
|
+
process.exit(1);
|
|
1843
|
+
}
|
|
1844
|
+
console.log(chalk_1.default.cyan(`Starting ${tunnelsToStart.length} tunnel(s) via ${globalProvider}...\n`));
|
|
1845
|
+
try {
|
|
1846
|
+
await startTunnelsFromConfig(tunnelsToStart, "", undefined, true, config);
|
|
1847
|
+
console.log(chalk_1.default.gray("\nPress Ctrl+C to stop"));
|
|
1848
|
+
// Keep running
|
|
1849
|
+
await new Promise(() => { });
|
|
1850
|
+
}
|
|
1851
|
+
catch (error) {
|
|
1852
|
+
console.log(chalk_1.default.red(`Failed to start tunnels: ${error.message}`));
|
|
1853
|
+
process.exit(1);
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1816
1856
|
if (isClientMode) {
|
|
1817
1857
|
// CLIENT MODE: Connect to remote server
|
|
1818
1858
|
// Build URL using basePath (default: op) -> wss://op.example.com/_tunnel
|
|
@@ -1888,6 +1928,7 @@ program
|
|
|
1888
1928
|
max: tcpMax,
|
|
1889
1929
|
},
|
|
1890
1930
|
selfSignedHttps: useHttps ? { enabled: true } : undefined,
|
|
1931
|
+
httpRedirect: useHttps, // Enable HTTP redirect when using HTTPS
|
|
1891
1932
|
// In hybrid mode, auth is not needed for localhost. For remote clients, still require token.
|
|
1892
1933
|
auth: config.server?.token && !isHybridMode ? { required: true, tokens: [config.server.token] } : undefined,
|
|
1893
1934
|
dymo: config.server?.dymo,
|
|
@@ -2643,8 +2684,16 @@ async function startTunnelsFromConfig(tunnels, serverUrl, token, insecure, globa
|
|
|
2643
2684
|
for (const tunnel of cloudflareTunnels) {
|
|
2644
2685
|
const spinner = (0, ora_1.default)(`Creating Cloudflare tunnel: ${tunnel.name}...`).start();
|
|
2645
2686
|
const cfHostname = tunnel.cfHostname || globalConfig?.cloudflare?.hostname;
|
|
2646
|
-
// For Cloudflare: subdomain = tunnel name
|
|
2687
|
+
// For Cloudflare: subdomain = tunnel name (for named tunnels)
|
|
2647
2688
|
const cfTunnelName = tunnel.subdomain || globalConfig?.cloudflare?.tunnelName;
|
|
2689
|
+
// Validate: named tunnels require cfHostname for public access
|
|
2690
|
+
if (cfTunnelName && !cfHostname) {
|
|
2691
|
+
spinner.fail(`${tunnel.name}: Named tunnel '${cfTunnelName}' requires cfHostname`);
|
|
2692
|
+
console.log(chalk_1.default.gray(` Add to your config:`));
|
|
2693
|
+
console.log(chalk_1.default.white(` cfHostname: ${cfTunnelName}.yourdomain.com`));
|
|
2694
|
+
console.log(chalk_1.default.gray(` Or remove 'subdomain' for a quick tunnel with random URL`));
|
|
2695
|
+
continue;
|
|
2696
|
+
}
|
|
2648
2697
|
// Merge IP access config: tunnel-specific > global security > none
|
|
2649
2698
|
const { IpFilter } = await Promise.resolve().then(() => __importStar(require("../shared/ip-filter")));
|
|
2650
2699
|
const ipAccess = IpFilter.mergeConfigs(globalConfig?.security?.ipAccess, tunnel.ipAccess);
|
|
@@ -2656,6 +2705,13 @@ async function startTunnelsFromConfig(tunnels, serverUrl, token, insecure, globa
|
|
|
2656
2705
|
});
|
|
2657
2706
|
try {
|
|
2658
2707
|
await client.connect();
|
|
2708
|
+
// Auto-route DNS if using named tunnel with hostname
|
|
2709
|
+
if (cfTunnelName && cfHostname) {
|
|
2710
|
+
const routeResult = await CloudflareTunnelClient_1.CloudflareTunnelClient.routeDns(cfTunnelName, cfHostname);
|
|
2711
|
+
if (!routeResult.success && !routeResult.error?.includes("already exists")) {
|
|
2712
|
+
spinner.warn(`${tunnel.name}: DNS routing failed: ${routeResult.error}`);
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2659
2715
|
const { tunnelId, publicUrl } = await client.createTunnel({
|
|
2660
2716
|
protocol: tunnel.protocol,
|
|
2661
2717
|
localHost: tunnel.host || "localhost",
|