opentunnel-cli 1.0.29 → 1.0.31
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 +44 -0
- package/dist/cli/index.js +63 -3
- 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/client/TunnelClient.d.ts.map +1 -1
- package/dist/client/TunnelClient.js +15 -7
- package/dist/client/TunnelClient.js.map +1 -1
- package/dist/server/TunnelServer.d.ts.map +1 -1
- package/dist/server/TunnelServer.js +19 -5
- package/dist/server/TunnelServer.js.map +1 -1
- package/dist/shared/types.d.ts +2 -0
- package/dist/shared/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
- [Server Mode](#server-mode-config)
|
|
19
19
|
- [Hybrid Mode](#hybrid-mode-config)
|
|
20
20
|
- [CLI Commands](#cli-commands)
|
|
21
|
+
- [Streaming Large Files / Video](#streaming-large-files--video)
|
|
21
22
|
- [Documentation](#documentation)
|
|
22
23
|
- [Architecture](#architecture)
|
|
23
24
|
- [Troubleshooting](#troubleshooting-quick)
|
|
@@ -144,6 +145,13 @@ tunnels:
|
|
|
144
145
|
protocol: http
|
|
145
146
|
port: 4000
|
|
146
147
|
subdomain: api
|
|
148
|
+
|
|
149
|
+
- name: media
|
|
150
|
+
protocol: http
|
|
151
|
+
port: 8080
|
|
152
|
+
subdomain: media
|
|
153
|
+
streaming: true # Enable streaming mode (10 min timeout)
|
|
154
|
+
# requestTimeout: 0 # Or no timeout (use with caution)
|
|
147
155
|
```
|
|
148
156
|
|
|
149
157
|
### Server Mode Config
|
|
@@ -219,6 +227,42 @@ tunnels:
|
|
|
219
227
|
- `-t, --token <token>` - Auth token
|
|
220
228
|
- `-n, --name <name>` - Custom subdomain
|
|
221
229
|
- `--insecure` - Skip SSL verification
|
|
230
|
+
- `--streaming` - Enable streaming mode (10 min timeout for video/large files)
|
|
231
|
+
- `--timeout <ms>` - Custom request timeout in milliseconds (0 = no timeout)
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Streaming Large Files / Video
|
|
236
|
+
|
|
237
|
+
For tunneling video streams or large file downloads, use the streaming options:
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
# Enable streaming mode (10 minute timeout)
|
|
241
|
+
opentunnel http 8080 -s example.com --streaming
|
|
242
|
+
|
|
243
|
+
# Custom timeout (5 minutes = 300000ms)
|
|
244
|
+
opentunnel http 8080 -s example.com --timeout 300000
|
|
245
|
+
|
|
246
|
+
# No timeout (use with caution - high memory usage)
|
|
247
|
+
opentunnel http 8080 -s example.com --timeout 0
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**In configuration file:**
|
|
251
|
+
|
|
252
|
+
```yaml
|
|
253
|
+
tunnels:
|
|
254
|
+
- name: media-server
|
|
255
|
+
protocol: http
|
|
256
|
+
port: 8080
|
|
257
|
+
subdomain: media
|
|
258
|
+
streaming: true # 10 min timeout
|
|
259
|
+
# requestTimeout: 300000 # Or custom timeout in ms
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
> **Note:** HTTP tunnels buffer the entire response in memory before forwarding. For very large files (>500MB), consider using TCP tunnels which stream byte-by-byte:
|
|
263
|
+
> ```bash
|
|
264
|
+
> opentunnel tcp 8080 -s example.com
|
|
265
|
+
> ```
|
|
222
266
|
|
|
223
267
|
---
|
|
224
268
|
|
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.31");
|
|
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
|
|
@@ -279,6 +279,8 @@ program
|
|
|
279
279
|
.option("-h, --host <host>", "Local host to forward to", "localhost")
|
|
280
280
|
.option("-t, --token <token>", "Authentication token (if server requires it)")
|
|
281
281
|
.option("--insecure", "Skip SSL certificate verification (for self-signed certs)")
|
|
282
|
+
.option("--streaming", "Enable streaming mode (longer timeouts for video/large files)")
|
|
283
|
+
.option("--timeout <ms>", "Custom request timeout in milliseconds")
|
|
282
284
|
.option("--local-server", "Start a local server before connecting")
|
|
283
285
|
.option("--server-port <port>", "Port for the local server (default: 443)", "443")
|
|
284
286
|
.action(async (port, options) => {
|
|
@@ -377,6 +379,8 @@ program
|
|
|
377
379
|
localHost: options.host,
|
|
378
380
|
localPort: parseInt(port),
|
|
379
381
|
subdomain: options.subdomain,
|
|
382
|
+
streaming: options.streaming || false,
|
|
383
|
+
requestTimeout: options.timeout ? parseInt(options.timeout) : undefined,
|
|
380
384
|
});
|
|
381
385
|
return { client, tunnelId, publicUrl };
|
|
382
386
|
};
|
|
@@ -579,6 +583,8 @@ program
|
|
|
579
583
|
localHost: options.host,
|
|
580
584
|
localPort: parseInt(port),
|
|
581
585
|
subdomain: options.subdomain,
|
|
586
|
+
streaming: options.streaming || false,
|
|
587
|
+
requestTimeout: options.timeout ? parseInt(options.timeout) : undefined,
|
|
582
588
|
});
|
|
583
589
|
spinner.succeed("Tunnel established!");
|
|
584
590
|
console.log("");
|
|
@@ -642,6 +648,8 @@ program
|
|
|
642
648
|
.option("--server-port <port>", "Server port", "443")
|
|
643
649
|
.option("--https", "Use HTTPS for local connection")
|
|
644
650
|
.option("--insecure", "Skip SSL verification (for self-signed certs)")
|
|
651
|
+
.option("--streaming", "Enable streaming mode (longer timeouts for video/large files)")
|
|
652
|
+
.option("--timeout <ms>", "Custom request timeout in milliseconds")
|
|
645
653
|
.option("--ngrok", "Use ngrok instead of OpenTunnel server")
|
|
646
654
|
.option("--region <region>", "Ngrok region (us, eu, ap, au, sa, jp, in)", "us")
|
|
647
655
|
.option("--cloudflare, --cf", "Use Cloudflare Tunnel instead of OpenTunnel server")
|
|
@@ -687,6 +695,8 @@ program
|
|
|
687
695
|
serverUrl,
|
|
688
696
|
token: options.token,
|
|
689
697
|
insecure: options.insecure,
|
|
698
|
+
streaming: options.streaming,
|
|
699
|
+
requestTimeout: options.timeout ? parseInt(options.timeout) : undefined,
|
|
690
700
|
});
|
|
691
701
|
return;
|
|
692
702
|
}
|
|
@@ -1809,7 +1819,15 @@ program
|
|
|
1809
1819
|
const isClientMode = mode === "client";
|
|
1810
1820
|
const isServerMode = mode === "server";
|
|
1811
1821
|
const isHybridMode = mode === "hybrid";
|
|
1812
|
-
if (
|
|
1822
|
+
// Check if using external provider (cloudflare/ngrok) which doesn't need domain/remote
|
|
1823
|
+
const globalProvider = config.provider || "opentunnel";
|
|
1824
|
+
const isExternalProvider = globalProvider === "cloudflare" || globalProvider === "ngrok";
|
|
1825
|
+
// Also check if all tunnels use external providers
|
|
1826
|
+
const allTunnelsExternal = hasTunnels && tunnelsToStart.every(t => {
|
|
1827
|
+
const provider = t.provider || globalProvider;
|
|
1828
|
+
return provider === "cloudflare" || provider === "ngrok";
|
|
1829
|
+
});
|
|
1830
|
+
if (!hasDomainConfig && !remote && !isExternalProvider && !allTunnelsExternal) {
|
|
1813
1831
|
console.log(chalk_1.default.red("Missing configuration."));
|
|
1814
1832
|
console.log(chalk_1.default.gray("\nAdd to your config:"));
|
|
1815
1833
|
console.log(chalk_1.default.cyan("\n # Run your own server:"));
|
|
@@ -1818,8 +1836,33 @@ program
|
|
|
1818
1836
|
console.log(chalk_1.default.cyan("\n # Or connect to a remote server:"));
|
|
1819
1837
|
console.log(chalk_1.default.white(" server:"));
|
|
1820
1838
|
console.log(chalk_1.default.white(" remote: example.com"));
|
|
1839
|
+
console.log(chalk_1.default.cyan("\n # Or use an external provider:"));
|
|
1840
|
+
console.log(chalk_1.default.white(" provider: cloudflare # or ngrok"));
|
|
1821
1841
|
process.exit(1);
|
|
1822
1842
|
}
|
|
1843
|
+
// External provider mode: skip OpenTunnel server, use cloudflare/ngrok directly
|
|
1844
|
+
if ((isExternalProvider || allTunnelsExternal) && !hasDomainConfig && !remote) {
|
|
1845
|
+
if (!hasTunnels) {
|
|
1846
|
+
console.log(chalk_1.default.red("No tunnels configured."));
|
|
1847
|
+
console.log(chalk_1.default.gray("\nAdd tunnels to your config:"));
|
|
1848
|
+
console.log(chalk_1.default.white(" tunnels:"));
|
|
1849
|
+
console.log(chalk_1.default.white(" - name: web"));
|
|
1850
|
+
console.log(chalk_1.default.white(" protocol: http"));
|
|
1851
|
+
console.log(chalk_1.default.white(" port: 80"));
|
|
1852
|
+
process.exit(1);
|
|
1853
|
+
}
|
|
1854
|
+
console.log(chalk_1.default.cyan(`Starting ${tunnelsToStart.length} tunnel(s) via ${globalProvider}...\n`));
|
|
1855
|
+
try {
|
|
1856
|
+
await startTunnelsFromConfig(tunnelsToStart, "", undefined, true, config);
|
|
1857
|
+
console.log(chalk_1.default.gray("\nPress Ctrl+C to stop"));
|
|
1858
|
+
// Keep running
|
|
1859
|
+
await new Promise(() => { });
|
|
1860
|
+
}
|
|
1861
|
+
catch (error) {
|
|
1862
|
+
console.log(chalk_1.default.red(`Failed to start tunnels: ${error.message}`));
|
|
1863
|
+
process.exit(1);
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1823
1866
|
if (isClientMode) {
|
|
1824
1867
|
// CLIENT MODE: Connect to remote server
|
|
1825
1868
|
// Build URL using basePath (default: op) -> wss://op.example.com/_tunnel
|
|
@@ -2651,8 +2694,16 @@ async function startTunnelsFromConfig(tunnels, serverUrl, token, insecure, globa
|
|
|
2651
2694
|
for (const tunnel of cloudflareTunnels) {
|
|
2652
2695
|
const spinner = (0, ora_1.default)(`Creating Cloudflare tunnel: ${tunnel.name}...`).start();
|
|
2653
2696
|
const cfHostname = tunnel.cfHostname || globalConfig?.cloudflare?.hostname;
|
|
2654
|
-
// For Cloudflare: subdomain = tunnel name
|
|
2697
|
+
// For Cloudflare: subdomain = tunnel name (for named tunnels)
|
|
2655
2698
|
const cfTunnelName = tunnel.subdomain || globalConfig?.cloudflare?.tunnelName;
|
|
2699
|
+
// Validate: named tunnels require cfHostname for public access
|
|
2700
|
+
if (cfTunnelName && !cfHostname) {
|
|
2701
|
+
spinner.fail(`${tunnel.name}: Named tunnel '${cfTunnelName}' requires cfHostname`);
|
|
2702
|
+
console.log(chalk_1.default.gray(` Add to your config:`));
|
|
2703
|
+
console.log(chalk_1.default.white(` cfHostname: ${cfTunnelName}.yourdomain.com`));
|
|
2704
|
+
console.log(chalk_1.default.gray(` Or remove 'subdomain' for a quick tunnel with random URL`));
|
|
2705
|
+
continue;
|
|
2706
|
+
}
|
|
2656
2707
|
// Merge IP access config: tunnel-specific > global security > none
|
|
2657
2708
|
const { IpFilter } = await Promise.resolve().then(() => __importStar(require("../shared/ip-filter")));
|
|
2658
2709
|
const ipAccess = IpFilter.mergeConfigs(globalConfig?.security?.ipAccess, tunnel.ipAccess);
|
|
@@ -2664,6 +2715,13 @@ async function startTunnelsFromConfig(tunnels, serverUrl, token, insecure, globa
|
|
|
2664
2715
|
});
|
|
2665
2716
|
try {
|
|
2666
2717
|
await client.connect();
|
|
2718
|
+
// Auto-route DNS if using named tunnel with hostname
|
|
2719
|
+
if (cfTunnelName && cfHostname) {
|
|
2720
|
+
const routeResult = await CloudflareTunnelClient_1.CloudflareTunnelClient.routeDns(cfTunnelName, cfHostname);
|
|
2721
|
+
if (!routeResult.success && !routeResult.error?.includes("already exists")) {
|
|
2722
|
+
spinner.warn(`${tunnel.name}: DNS routing failed: ${routeResult.error}`);
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2667
2725
|
const { tunnelId, publicUrl } = await client.createTunnel({
|
|
2668
2726
|
protocol: tunnel.protocol,
|
|
2669
2727
|
localHost: tunnel.host || "localhost",
|
|
@@ -2823,6 +2881,8 @@ async function createTunnel(options) {
|
|
|
2823
2881
|
localPort: options.localPort,
|
|
2824
2882
|
subdomain: options.subdomain,
|
|
2825
2883
|
remotePort: options.remotePort,
|
|
2884
|
+
streaming: options.streaming,
|
|
2885
|
+
requestTimeout: options.requestTimeout,
|
|
2826
2886
|
});
|
|
2827
2887
|
spinner.succeed("Tunnel established!");
|
|
2828
2888
|
printTunnelInfo({
|