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 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: 8080)
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.28");
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(`✓ Server running on port ${serverPort}\n`));
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 (!hasDomainConfig && !remote) {
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",