opentunnel-cli 1.0.6 → 1.0.8
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 +130 -84
- package/dist/cli/index.js +255 -152
- package/dist/cli/index.js.map +1 -1
- package/dist/client/TunnelClient.d.ts +1 -0
- package/dist/client/TunnelClient.d.ts.map +1 -1
- package/dist/client/TunnelClient.js +129 -10
- package/dist/client/TunnelClient.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -44,15 +44,72 @@ const TunnelClient_1 = require("../client/TunnelClient");
|
|
|
44
44
|
const NgrokClient_1 = require("../client/NgrokClient");
|
|
45
45
|
const utils_1 = require("../shared/utils");
|
|
46
46
|
const yaml_1 = require("yaml");
|
|
47
|
+
const fs = __importStar(require("fs"));
|
|
48
|
+
const path = __importStar(require("path"));
|
|
47
49
|
const CONFIG_FILE = "opentunnel.yml";
|
|
50
|
+
// Load .env file if exists
|
|
51
|
+
function loadEnvFile() {
|
|
52
|
+
const envPath = path.join(process.cwd(), ".env");
|
|
53
|
+
if (fs.existsSync(envPath)) {
|
|
54
|
+
const envContent = fs.readFileSync(envPath, "utf-8");
|
|
55
|
+
for (const line of envContent.split("\n")) {
|
|
56
|
+
const trimmed = line.trim();
|
|
57
|
+
// Skip comments and empty lines
|
|
58
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
59
|
+
continue;
|
|
60
|
+
const match = trimmed.match(/^([^=]+)=(.*)$/);
|
|
61
|
+
if (match) {
|
|
62
|
+
const key = match[1].trim();
|
|
63
|
+
let value = match[2].trim();
|
|
64
|
+
// Remove surrounding quotes if present
|
|
65
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
66
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
67
|
+
value = value.slice(1, -1);
|
|
68
|
+
}
|
|
69
|
+
// Only set if not already defined in environment
|
|
70
|
+
if (process.env[key] === undefined) {
|
|
71
|
+
process.env[key] = value;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Docker-style environment variable substitution
|
|
78
|
+
// Supports: ${VAR}, ${VAR:-default}, ${VAR:=default}
|
|
79
|
+
function substituteEnvVars(content) {
|
|
80
|
+
// Pattern matches ${VAR}, ${VAR:-default}, ${VAR:=default}
|
|
81
|
+
const pattern = /\$\{([^}:]+)(?:(:[-=])([^}]*))?\}/g;
|
|
82
|
+
return content.replace(pattern, (match, varName, operator, defaultValue) => {
|
|
83
|
+
const envValue = process.env[varName];
|
|
84
|
+
if (operator === ":-" || operator === ":=") {
|
|
85
|
+
// Use default if variable is unset or empty
|
|
86
|
+
return (envValue !== undefined && envValue !== "") ? envValue : (defaultValue || "");
|
|
87
|
+
}
|
|
88
|
+
// Just ${VAR} - return value or empty string
|
|
89
|
+
return envValue || "";
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
// Load and parse config file with environment variable substitution
|
|
93
|
+
function loadConfig(configPath) {
|
|
94
|
+
if (!fs.existsSync(configPath)) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
// Load .env file first
|
|
98
|
+
loadEnvFile();
|
|
99
|
+
// Read and substitute environment variables
|
|
100
|
+
let content = fs.readFileSync(configPath, "utf-8");
|
|
101
|
+
content = substituteEnvVars(content);
|
|
102
|
+
return (0, yaml_1.parse)(content);
|
|
103
|
+
}
|
|
48
104
|
const program = new commander_1.Command();
|
|
49
105
|
program
|
|
50
106
|
.name("opentunnel")
|
|
51
107
|
.alias("ot")
|
|
52
108
|
.description("Expose local ports to the internet via custom domains or ngrok")
|
|
53
109
|
.version("1.0.0");
|
|
54
|
-
// Helper function to build WebSocket URL from
|
|
55
|
-
|
|
110
|
+
// Helper function to build WebSocket URL from domain
|
|
111
|
+
// User only provides base domain (e.g., fjrg2007.com), system handles the rest
|
|
112
|
+
function buildServerUrl(server, insecure, basePath) {
|
|
56
113
|
let hostname = server;
|
|
57
114
|
// Remove protocol if provided
|
|
58
115
|
hostname = hostname.replace(/^(wss?|https?):\/\//, "");
|
|
@@ -60,9 +117,14 @@ function buildServerUrl(server, insecure) {
|
|
|
60
117
|
hostname = hostname.replace(/\/_tunnel.*$/, "");
|
|
61
118
|
// Remove trailing slash
|
|
62
119
|
hostname = hostname.replace(/\/$/, "");
|
|
120
|
+
// Build the full hostname with basePath if provided and not empty
|
|
121
|
+
// If basePath is "op" (default), connect to op.domain.com
|
|
122
|
+
// If basePath is empty or not provided, connect directly to domain.com
|
|
123
|
+
const effectiveBasePath = basePath || "op";
|
|
124
|
+
const fullHostname = effectiveBasePath ? `${effectiveBasePath}.${hostname}` : hostname;
|
|
63
125
|
const protocol = insecure ? "ws" : "wss";
|
|
64
126
|
return {
|
|
65
|
-
url: `${protocol}://${
|
|
127
|
+
url: `${protocol}://${fullHostname}/_tunnel`,
|
|
66
128
|
displayName: hostname,
|
|
67
129
|
};
|
|
68
130
|
}
|
|
@@ -70,15 +132,16 @@ function buildServerUrl(server, insecure) {
|
|
|
70
132
|
program
|
|
71
133
|
.command("quick <port>")
|
|
72
134
|
.description("Instantly expose a local port to the internet")
|
|
73
|
-
.requiredOption("-s, --server <
|
|
135
|
+
.requiredOption("-s, --server <domain>", "Server domain (e.g., example.com)")
|
|
136
|
+
.option("-b, --base-path <path>", "Server base path (default: op)")
|
|
74
137
|
.option("-n, --subdomain <name>", "Request a specific subdomain (e.g., 'myapp')")
|
|
75
138
|
.option("-p, --protocol <proto>", "Protocol (http, https, tcp)", "http")
|
|
76
139
|
.option("-h, --host <host>", "Local host to forward to", "localhost")
|
|
77
140
|
.option("-t, --token <token>", "Authentication token (if server requires it)")
|
|
78
141
|
.option("--insecure", "Skip SSL certificate verification (for self-signed certs)")
|
|
79
142
|
.action(async (port, options) => {
|
|
80
|
-
// Build server URL from
|
|
81
|
-
const { url: serverUrl, displayName: serverDisplayName } = buildServerUrl(options.server, options.insecure);
|
|
143
|
+
// Build server URL from domain (user provides domain, system adds basePath)
|
|
144
|
+
const { url: serverUrl, displayName: serverDisplayName } = buildServerUrl(options.server, options.insecure, options.basePath);
|
|
82
145
|
console.log(chalk_1.default.cyan(`
|
|
83
146
|
██████╗ ██████╗ ███████╗███╗ ██╗████████╗██╗ ██╗███╗ ██╗███╗ ██╗███████╗██╗
|
|
84
147
|
██╔═══██╗██╔══██╗██╔════╝████╗ ██║╚══██╔══╝██║ ██║████╗ ██║████╗ ██║██╔════╝██║
|
|
@@ -158,14 +221,14 @@ program
|
|
|
158
221
|
program
|
|
159
222
|
.command("http <port>")
|
|
160
223
|
.description("Expose a local HTTP server")
|
|
161
|
-
.option("-s, --server <
|
|
224
|
+
.option("-s, --server <domain>", "Remote server domain (if not provided, starts local server)")
|
|
225
|
+
.option("-b, --base-path <path>", "Server base path (default: op)")
|
|
162
226
|
.option("-t, --token <token>", "Authentication token")
|
|
163
227
|
.option("-n, --subdomain <name>", "Custom subdomain (e.g., 'myapp' for myapp.op.domain.com)")
|
|
164
228
|
.option("-d, --detach", "Run tunnel in background")
|
|
165
229
|
.option("-h, --host <host>", "Local host", "localhost")
|
|
166
230
|
.option("--domain <domain>", "Domain for the tunnel", "localhost")
|
|
167
231
|
.option("--port <port>", "Server port", "443")
|
|
168
|
-
.option("--base-path <path>", "Subdomain base path", "op")
|
|
169
232
|
.option("--https", "Use HTTPS for local connection")
|
|
170
233
|
.option("--insecure", "Skip SSL verification (for self-signed certs)")
|
|
171
234
|
.option("--ngrok", "Use ngrok instead of OpenTunnel server")
|
|
@@ -182,9 +245,9 @@ program
|
|
|
182
245
|
});
|
|
183
246
|
return;
|
|
184
247
|
}
|
|
185
|
-
// If remote server
|
|
248
|
+
// If remote server domain provided, just connect to it
|
|
186
249
|
if (options.server) {
|
|
187
|
-
const { url: serverUrl } = buildServerUrl(options.server, options.insecure);
|
|
250
|
+
const { url: serverUrl } = buildServerUrl(options.server, options.insecure, options.basePath);
|
|
188
251
|
await createTunnel({
|
|
189
252
|
protocol: options.https ? "https" : "http",
|
|
190
253
|
localHost: options.host,
|
|
@@ -245,7 +308,8 @@ program
|
|
|
245
308
|
program
|
|
246
309
|
.command("tcp <port>")
|
|
247
310
|
.description("Expose a local TCP server")
|
|
248
|
-
.option("-s, --server <
|
|
311
|
+
.option("-s, --server <domain>", "Remote server domain (if not provided, starts local server)")
|
|
312
|
+
.option("-b, --base-path <path>", "Server base path (default: op)")
|
|
249
313
|
.option("-t, --token <token>", "Authentication token")
|
|
250
314
|
.option("-r, --remote-port <port>", "Remote port to use")
|
|
251
315
|
.option("-h, --host <host>", "Local host", "localhost")
|
|
@@ -266,9 +330,9 @@ program
|
|
|
266
330
|
});
|
|
267
331
|
return;
|
|
268
332
|
}
|
|
269
|
-
// If remote server
|
|
333
|
+
// If remote server domain provided, just connect to it
|
|
270
334
|
if (options.server) {
|
|
271
|
-
const { url: serverUrl } = buildServerUrl(options.server, options.insecure);
|
|
335
|
+
const { url: serverUrl } = buildServerUrl(options.server, options.insecure, options.basePath);
|
|
272
336
|
await createTunnel({
|
|
273
337
|
protocol: "tcp",
|
|
274
338
|
localHost: options.host,
|
|
@@ -367,13 +431,13 @@ program
|
|
|
367
431
|
program
|
|
368
432
|
.command("server")
|
|
369
433
|
.description("Start the OpenTunnel server (standalone mode)")
|
|
370
|
-
.option("-p, --port <port>", "Server port"
|
|
434
|
+
.option("-p, --port <port>", "Server port")
|
|
371
435
|
.option("--public-port <port>", "Public port shown in URLs (default: same as port)")
|
|
372
|
-
.option("--domain <domain>", "Base domain"
|
|
373
|
-
.option("-b, --base-path <path>", "Subdomain base path (e.g., 'op' for *.op.domain.com)"
|
|
374
|
-
.option("--host <host>", "Bind host"
|
|
375
|
-
.option("--tcp-min <port>", "Minimum TCP port"
|
|
376
|
-
.option("--tcp-max <port>", "Maximum TCP port"
|
|
436
|
+
.option("--domain <domain>", "Base domain")
|
|
437
|
+
.option("-b, --base-path <path>", "Subdomain base path (e.g., 'op' for *.op.domain.com)")
|
|
438
|
+
.option("--host <host>", "Bind host")
|
|
439
|
+
.option("--tcp-min <port>", "Minimum TCP port")
|
|
440
|
+
.option("--tcp-max <port>", "Maximum TCP port")
|
|
377
441
|
.option("--auth-tokens <tokens>", "Comma-separated auth tokens")
|
|
378
442
|
.option("--no-https", "Disable HTTPS (use plain HTTP)")
|
|
379
443
|
.option("--https-cert <path>", "Path to SSL certificate (for custom certs)")
|
|
@@ -385,16 +449,48 @@ program
|
|
|
385
449
|
.option("--duckdns-token <token>", "DuckDNS token for dynamic DNS updates")
|
|
386
450
|
.option("-d, --detach", "Run server in background (detached mode)")
|
|
387
451
|
.action(async (options) => {
|
|
452
|
+
// Load config from opentunnel.yml if exists (with env variable substitution)
|
|
453
|
+
const configPath = path.join(process.cwd(), CONFIG_FILE);
|
|
454
|
+
let fileConfig = {};
|
|
455
|
+
try {
|
|
456
|
+
const parsed = loadConfig(configPath);
|
|
457
|
+
if (parsed?.server && parsed.server.domain) {
|
|
458
|
+
fileConfig = parsed.server;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
catch (err) {
|
|
462
|
+
// Ignore parse errors, use CLI options
|
|
463
|
+
}
|
|
464
|
+
// Merge config: CLI options override file config, then defaults
|
|
465
|
+
const mergedOptions = {
|
|
466
|
+
port: options.port || fileConfig.port?.toString() || "443",
|
|
467
|
+
publicPort: options.publicPort || fileConfig.publicPort?.toString(),
|
|
468
|
+
domain: options.domain || fileConfig.domain || "localhost",
|
|
469
|
+
basePath: options.basePath || fileConfig.basePath || "op",
|
|
470
|
+
host: options.host || fileConfig.host || "0.0.0.0",
|
|
471
|
+
tcpMin: options.tcpMin || fileConfig.tcpPortMin?.toString() || "10000",
|
|
472
|
+
tcpMax: options.tcpMax || fileConfig.tcpPortMax?.toString() || "20000",
|
|
473
|
+
authTokens: options.authTokens || fileConfig.token,
|
|
474
|
+
https: options.https !== false && fileConfig.https !== false,
|
|
475
|
+
httpsCert: options.httpsCert,
|
|
476
|
+
httpsKey: options.httpsKey,
|
|
477
|
+
letsencrypt: options.letsencrypt,
|
|
478
|
+
email: options.email,
|
|
479
|
+
production: options.production,
|
|
480
|
+
cloudflareToken: options.cloudflareToken,
|
|
481
|
+
duckdnsToken: options.duckdnsToken,
|
|
482
|
+
detach: options.detach,
|
|
483
|
+
};
|
|
388
484
|
// Detached mode - run in background
|
|
389
|
-
if (
|
|
485
|
+
if (mergedOptions.detach) {
|
|
390
486
|
const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
|
|
391
|
-
const
|
|
392
|
-
const
|
|
393
|
-
const pidFile =
|
|
394
|
-
const logFile =
|
|
487
|
+
const fsAsync = await Promise.resolve().then(() => __importStar(require("fs")));
|
|
488
|
+
const pathAsync = await Promise.resolve().then(() => __importStar(require("path")));
|
|
489
|
+
const pidFile = pathAsync.join(process.cwd(), ".opentunnel.pid");
|
|
490
|
+
const logFile = pathAsync.join(process.cwd(), "opentunnel.log");
|
|
395
491
|
// Check if already running
|
|
396
|
-
if (
|
|
397
|
-
const oldPid =
|
|
492
|
+
if (fsAsync.existsSync(pidFile)) {
|
|
493
|
+
const oldPid = fsAsync.readFileSync(pidFile, "utf-8").trim();
|
|
398
494
|
try {
|
|
399
495
|
process.kill(parseInt(oldPid), 0);
|
|
400
496
|
console.log(chalk_1.default.yellow(`Server already running (PID: ${oldPid})`));
|
|
@@ -402,60 +498,48 @@ program
|
|
|
402
498
|
return;
|
|
403
499
|
}
|
|
404
500
|
catch {
|
|
405
|
-
|
|
501
|
+
fsAsync.unlinkSync(pidFile);
|
|
406
502
|
}
|
|
407
503
|
}
|
|
408
|
-
// Build args without -d flag
|
|
504
|
+
// Build args without -d flag, using merged options
|
|
409
505
|
const args = ["server"];
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
if (
|
|
417
|
-
args.push("-
|
|
418
|
-
if (
|
|
419
|
-
args.push("--
|
|
420
|
-
if (
|
|
421
|
-
args.push("--tcp-min", options.tcpMin);
|
|
422
|
-
if (options.tcpMax)
|
|
423
|
-
args.push("--tcp-max", options.tcpMax);
|
|
424
|
-
if (options.authTokens)
|
|
425
|
-
args.push("--auth-tokens", options.authTokens);
|
|
426
|
-
if (options.https)
|
|
506
|
+
args.push("-p", mergedOptions.port);
|
|
507
|
+
args.push("--domain", mergedOptions.domain);
|
|
508
|
+
args.push("-b", mergedOptions.basePath);
|
|
509
|
+
args.push("--host", mergedOptions.host);
|
|
510
|
+
args.push("--tcp-min", mergedOptions.tcpMin);
|
|
511
|
+
args.push("--tcp-max", mergedOptions.tcpMax);
|
|
512
|
+
if (mergedOptions.publicPort)
|
|
513
|
+
args.push("--public-port", mergedOptions.publicPort);
|
|
514
|
+
if (mergedOptions.authTokens)
|
|
515
|
+
args.push("--auth-tokens", mergedOptions.authTokens);
|
|
516
|
+
if (mergedOptions.https)
|
|
427
517
|
args.push("--https");
|
|
428
|
-
if (
|
|
429
|
-
args.push("--email",
|
|
430
|
-
if (
|
|
518
|
+
if (mergedOptions.email)
|
|
519
|
+
args.push("--email", mergedOptions.email);
|
|
520
|
+
if (mergedOptions.production)
|
|
431
521
|
args.push("--production");
|
|
432
|
-
if (
|
|
433
|
-
args.push("--cloudflare-token",
|
|
434
|
-
if (
|
|
435
|
-
args.push("--duckdns-token",
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
if (options.dnsCreateRecords)
|
|
439
|
-
args.push("--dns-create-records");
|
|
440
|
-
if (options.dnsDeleteOnClose)
|
|
441
|
-
args.push("--dns-delete-on-close");
|
|
442
|
-
const out = fs.openSync(logFile, "a");
|
|
443
|
-
const err = fs.openSync(logFile, "a");
|
|
522
|
+
if (mergedOptions.cloudflareToken)
|
|
523
|
+
args.push("--cloudflare-token", mergedOptions.cloudflareToken);
|
|
524
|
+
if (mergedOptions.duckdnsToken)
|
|
525
|
+
args.push("--duckdns-token", mergedOptions.duckdnsToken);
|
|
526
|
+
const out = fsAsync.openSync(logFile, "a");
|
|
527
|
+
const err = fsAsync.openSync(logFile, "a");
|
|
444
528
|
const child = spawn(process.execPath, [process.argv[1], ...args], {
|
|
445
529
|
detached: true,
|
|
446
530
|
stdio: ["ignore", out, err],
|
|
447
531
|
cwd: process.cwd(),
|
|
448
532
|
});
|
|
449
533
|
child.unref();
|
|
450
|
-
|
|
534
|
+
fsAsync.writeFileSync(pidFile, String(child.pid));
|
|
451
535
|
console.log(chalk_1.default.green(`OpenTunnel server started in background`));
|
|
452
536
|
console.log(chalk_1.default.gray(` PID: ${child.pid}`));
|
|
453
|
-
console.log(chalk_1.default.gray(` Port: ${
|
|
454
|
-
console.log(chalk_1.default.gray(` Domain: ${
|
|
537
|
+
console.log(chalk_1.default.gray(` Port: ${mergedOptions.port}`));
|
|
538
|
+
console.log(chalk_1.default.gray(` Domain: ${mergedOptions.domain}`));
|
|
455
539
|
console.log(chalk_1.default.gray(` Log: ${logFile}`));
|
|
456
540
|
console.log(chalk_1.default.gray(` PID file: ${pidFile}`));
|
|
457
541
|
console.log("");
|
|
458
|
-
console.log(chalk_1.default.gray(`Stop with:
|
|
542
|
+
console.log(chalk_1.default.gray(`Stop with: opentunnel stop`));
|
|
459
543
|
console.log(chalk_1.default.gray(`Logs: tail -f ${logFile}`));
|
|
460
544
|
return;
|
|
461
545
|
}
|
|
@@ -465,46 +549,46 @@ program
|
|
|
465
549
|
let httpsConfig = undefined;
|
|
466
550
|
let selfSignedHttpsConfig = undefined;
|
|
467
551
|
let autoHttpsConfig = undefined;
|
|
468
|
-
if (
|
|
552
|
+
if (mergedOptions.httpsCert && mergedOptions.httpsKey) {
|
|
469
553
|
// Custom certificates provided
|
|
470
|
-
const
|
|
554
|
+
const fsRead = await Promise.resolve().then(() => __importStar(require("fs")));
|
|
471
555
|
httpsConfig = {
|
|
472
|
-
cert:
|
|
473
|
-
key:
|
|
556
|
+
cert: fsRead.readFileSync(mergedOptions.httpsCert, "utf-8"),
|
|
557
|
+
key: fsRead.readFileSync(mergedOptions.httpsKey, "utf-8"),
|
|
474
558
|
};
|
|
475
559
|
}
|
|
476
|
-
else if (
|
|
560
|
+
else if (mergedOptions.letsencrypt) {
|
|
477
561
|
// Let's Encrypt
|
|
478
562
|
autoHttpsConfig = {
|
|
479
563
|
enabled: true,
|
|
480
|
-
email:
|
|
481
|
-
production:
|
|
482
|
-
cloudflareToken:
|
|
564
|
+
email: mergedOptions.email || `admin@${mergedOptions.domain}`,
|
|
565
|
+
production: mergedOptions.production || false,
|
|
566
|
+
cloudflareToken: mergedOptions.cloudflareToken,
|
|
483
567
|
};
|
|
484
568
|
}
|
|
485
569
|
else {
|
|
486
570
|
// Self-signed by default (use --no-https to disable)
|
|
487
571
|
selfSignedHttpsConfig = {
|
|
488
|
-
enabled:
|
|
572
|
+
enabled: mergedOptions.https !== false,
|
|
489
573
|
};
|
|
490
574
|
}
|
|
491
575
|
const server = new TunnelServer({
|
|
492
|
-
port: parseInt(
|
|
493
|
-
publicPort:
|
|
494
|
-
host:
|
|
495
|
-
domain:
|
|
496
|
-
basePath:
|
|
576
|
+
port: parseInt(mergedOptions.port),
|
|
577
|
+
publicPort: mergedOptions.publicPort ? parseInt(mergedOptions.publicPort) : undefined,
|
|
578
|
+
host: mergedOptions.host,
|
|
579
|
+
domain: mergedOptions.domain,
|
|
580
|
+
basePath: mergedOptions.basePath,
|
|
497
581
|
tunnelPortRange: {
|
|
498
|
-
min: parseInt(
|
|
499
|
-
max: parseInt(
|
|
582
|
+
min: parseInt(mergedOptions.tcpMin),
|
|
583
|
+
max: parseInt(mergedOptions.tcpMax),
|
|
500
584
|
},
|
|
501
|
-
auth:
|
|
502
|
-
? { required: true, tokens:
|
|
585
|
+
auth: mergedOptions.authTokens
|
|
586
|
+
? { required: true, tokens: mergedOptions.authTokens.split(",") }
|
|
503
587
|
: undefined,
|
|
504
588
|
https: httpsConfig,
|
|
505
589
|
selfSignedHttps: selfSignedHttpsConfig,
|
|
506
590
|
autoHttps: autoHttpsConfig,
|
|
507
|
-
autoDns: detectDnsConfig(
|
|
591
|
+
autoDns: detectDnsConfig(mergedOptions),
|
|
508
592
|
});
|
|
509
593
|
// Helper function to auto-detect DNS provider
|
|
510
594
|
function detectDnsConfig(opts) {
|
|
@@ -517,8 +601,8 @@ program
|
|
|
517
601
|
enabled: true,
|
|
518
602
|
provider: "cloudflare",
|
|
519
603
|
cloudflareToken: opts.cloudflareToken,
|
|
520
|
-
createRecords:
|
|
521
|
-
deleteOnClose:
|
|
604
|
+
createRecords: false,
|
|
605
|
+
deleteOnClose: false,
|
|
522
606
|
setupWildcard: true,
|
|
523
607
|
};
|
|
524
608
|
}
|
|
@@ -527,15 +611,11 @@ program
|
|
|
527
611
|
enabled: true,
|
|
528
612
|
provider: "duckdns",
|
|
529
613
|
duckdnsToken: opts.duckdnsToken,
|
|
530
|
-
createRecords: false,
|
|
614
|
+
createRecords: false,
|
|
531
615
|
deleteOnClose: false,
|
|
532
616
|
setupWildcard: false,
|
|
533
617
|
};
|
|
534
618
|
}
|
|
535
|
-
// No auto DNS if no tokens provided
|
|
536
|
-
if (opts.autoDns) {
|
|
537
|
-
console.log(chalk_1.default.yellow("Warning: --auto-dns requires --cloudflare-token or --duckdns-token"));
|
|
538
|
-
}
|
|
539
619
|
return undefined;
|
|
540
620
|
}
|
|
541
621
|
console.log(chalk_1.default.cyan(`
|
|
@@ -553,10 +633,10 @@ program
|
|
|
553
633
|
console.log(chalk_1.default.yellow(`[-] Tunnel closed: ${tunnelId}`));
|
|
554
634
|
});
|
|
555
635
|
await server.start();
|
|
556
|
-
console.log(chalk_1.default.green(`\nServer running on ${
|
|
557
|
-
console.log(chalk_1.default.gray(`Domain: ${
|
|
558
|
-
console.log(chalk_1.default.gray(`Subdomain pattern: *.${
|
|
559
|
-
console.log(chalk_1.default.gray(`TCP port range: ${
|
|
636
|
+
console.log(chalk_1.default.green(`\nServer running on ${mergedOptions.host}:${mergedOptions.port}`));
|
|
637
|
+
console.log(chalk_1.default.gray(`Domain: ${mergedOptions.domain}`));
|
|
638
|
+
console.log(chalk_1.default.gray(`Subdomain pattern: *.${mergedOptions.basePath}.${mergedOptions.domain}`));
|
|
639
|
+
console.log(chalk_1.default.gray(`TCP port range: ${mergedOptions.tcpMin}-${mergedOptions.tcpMax}\n`));
|
|
560
640
|
});
|
|
561
641
|
// Stop command
|
|
562
642
|
program
|
|
@@ -879,76 +959,99 @@ program
|
|
|
879
959
|
.command("init")
|
|
880
960
|
.description("Create an example opentunnel.yml configuration file")
|
|
881
961
|
.option("-f, --force", "Overwrite existing config file")
|
|
962
|
+
.option("--server", "Create server mode config")
|
|
963
|
+
.option("--client", "Create client mode config (default)")
|
|
882
964
|
.action(async (options) => {
|
|
883
|
-
const
|
|
884
|
-
const
|
|
885
|
-
const configPath =
|
|
886
|
-
|
|
965
|
+
const fsInit = await Promise.resolve().then(() => __importStar(require("fs")));
|
|
966
|
+
const pathInit = await Promise.resolve().then(() => __importStar(require("path")));
|
|
967
|
+
const configPath = pathInit.join(process.cwd(), CONFIG_FILE);
|
|
968
|
+
const envPath = pathInit.join(process.cwd(), ".env");
|
|
969
|
+
if (fsInit.existsSync(configPath) && !options.force) {
|
|
887
970
|
console.log(chalk_1.default.yellow(`Config file already exists: ${configPath}`));
|
|
888
971
|
console.log(chalk_1.default.gray("Use --force to overwrite"));
|
|
889
972
|
return;
|
|
890
973
|
}
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
974
|
+
// Example config with environment variable syntax
|
|
975
|
+
const clientConfig = `# OpenTunnel Client Configuration
|
|
976
|
+
# Supports environment variables: \${VAR} or \${VAR:-default}
|
|
977
|
+
|
|
978
|
+
server:
|
|
979
|
+
remote: \${SERVER_DOMAIN:-example.com} # Server domain (system adds basePath)
|
|
980
|
+
token: \${AUTH_TOKEN} # From .env (optional)
|
|
981
|
+
|
|
982
|
+
tunnels:
|
|
983
|
+
- name: web
|
|
984
|
+
protocol: http
|
|
985
|
+
port: 3000
|
|
986
|
+
subdomain: web # → web.op.example.com
|
|
987
|
+
|
|
988
|
+
- name: api
|
|
989
|
+
protocol: http
|
|
990
|
+
port: 4000
|
|
991
|
+
subdomain: api # → api.op.example.com
|
|
992
|
+
|
|
993
|
+
- name: database
|
|
994
|
+
protocol: tcp
|
|
995
|
+
port: 5432
|
|
996
|
+
remotePort: 15432 # → example.com:15432
|
|
997
|
+
autostart: false # Don't start automatically
|
|
998
|
+
`;
|
|
999
|
+
const serverConfig = `# OpenTunnel Server Configuration
|
|
1000
|
+
# Supports environment variables: \${VAR} or \${VAR:-default}
|
|
1001
|
+
|
|
1002
|
+
server:
|
|
1003
|
+
domain: \${DOMAIN:-example.com} # Your base domain
|
|
1004
|
+
token: \${AUTH_TOKEN} # From .env (optional for public server)
|
|
1005
|
+
# tcpPortMin: 10000 # TCP tunnel port range (optional)
|
|
1006
|
+
# tcpPortMax: 20000
|
|
1007
|
+
`;
|
|
1008
|
+
const envExample = `# OpenTunnel Environment Variables
|
|
1009
|
+
# Copy to .env and fill in your values
|
|
1010
|
+
|
|
1011
|
+
# Server/Remote domain
|
|
1012
|
+
DOMAIN=example.com
|
|
1013
|
+
SERVER_DOMAIN=example.com
|
|
1014
|
+
|
|
1015
|
+
# Authentication token (leave empty for public server)
|
|
1016
|
+
AUTH_TOKEN=
|
|
1017
|
+
`;
|
|
1018
|
+
const configContent = options.server ? serverConfig : clientConfig;
|
|
1019
|
+
fsInit.writeFileSync(configPath, configContent);
|
|
1020
|
+
// Create .env.example if it doesn't exist
|
|
1021
|
+
const envExamplePath = pathInit.join(process.cwd(), ".env.example");
|
|
1022
|
+
if (!fsInit.existsSync(envExamplePath) && !fsInit.existsSync(envPath)) {
|
|
1023
|
+
fsInit.writeFileSync(envExamplePath, envExample);
|
|
1024
|
+
console.log(chalk_1.default.green(`Created .env.example`));
|
|
1025
|
+
}
|
|
922
1026
|
console.log(chalk_1.default.green(`Created ${CONFIG_FILE}`));
|
|
923
|
-
console.log(chalk_1.default.gray(`\
|
|
1027
|
+
console.log(chalk_1.default.gray(`\nEnvironment variables supported:`));
|
|
1028
|
+
console.log(chalk_1.default.cyan(` \${VAR} → Use value of VAR`));
|
|
1029
|
+
console.log(chalk_1.default.cyan(` \${VAR:-default} → Use VAR or "default" if not set`));
|
|
1030
|
+
console.log(chalk_1.default.gray(`\nCreate a .env file for secrets, then run:`));
|
|
924
1031
|
console.log(chalk_1.default.cyan(` opentunnel up # Start all tunnels`));
|
|
925
1032
|
console.log(chalk_1.default.cyan(` opentunnel up -d # Start in background`));
|
|
926
1033
|
});
|
|
927
|
-
// Up command - start tunnels from config
|
|
1034
|
+
// Up command - start tunnels from config
|
|
928
1035
|
program
|
|
929
1036
|
.command("up")
|
|
930
|
-
.description("Start server and tunnels from opentunnel.yml
|
|
1037
|
+
.description("Start server and tunnels from opentunnel.yml")
|
|
931
1038
|
.option("-d, --detach", "Run in background (detached mode)")
|
|
932
1039
|
.option("-f, --file <path>", "Config file path", CONFIG_FILE)
|
|
933
1040
|
.option("--no-autostart", "Ignore autostart setting, start all tunnels")
|
|
934
1041
|
.action(async (options) => {
|
|
935
|
-
const
|
|
936
|
-
const
|
|
937
|
-
// Load config file
|
|
938
|
-
const configPath =
|
|
939
|
-
|
|
940
|
-
if (
|
|
941
|
-
const configContent = fs.readFileSync(configPath, "utf-8");
|
|
942
|
-
config = (0, yaml_1.parse)(configContent);
|
|
943
|
-
}
|
|
944
|
-
else {
|
|
1042
|
+
const fsModule = await Promise.resolve().then(() => __importStar(require("fs")));
|
|
1043
|
+
const pathModule = await Promise.resolve().then(() => __importStar(require("path")));
|
|
1044
|
+
// Load config file (with env variable substitution)
|
|
1045
|
+
const configPath = pathModule.join(process.cwd(), options.file);
|
|
1046
|
+
const config = loadConfig(configPath);
|
|
1047
|
+
if (!config) {
|
|
945
1048
|
console.log(chalk_1.default.red(`Config file not found: ${configPath}`));
|
|
946
1049
|
console.log(chalk_1.default.gray(`Run 'opentunnel init' to create one`));
|
|
947
1050
|
return;
|
|
948
1051
|
}
|
|
949
1052
|
const tunnelsToStart = options.autostart === false
|
|
950
|
-
? config.tunnels
|
|
951
|
-
: config.tunnels?.filter(t => t.autostart !== false) || [];
|
|
1053
|
+
? (config.tunnels || [])
|
|
1054
|
+
: (config.tunnels?.filter(t => t.autostart !== false) || []);
|
|
952
1055
|
// Display banner
|
|
953
1056
|
console.log(chalk_1.default.cyan(`
|
|
954
1057
|
██████╗ ██████╗ ███████╗███╗ ██╗████████╗██╗ ██╗███╗ ██╗███╗ ██╗███████╗██╗
|
|
@@ -975,16 +1078,16 @@ program
|
|
|
975
1078
|
console.log(chalk_1.default.gray("\nAdd to your config:"));
|
|
976
1079
|
console.log(chalk_1.default.cyan("\n # Run your own server:"));
|
|
977
1080
|
console.log(chalk_1.default.white(" server:"));
|
|
978
|
-
console.log(chalk_1.default.white(" domain:
|
|
1081
|
+
console.log(chalk_1.default.white(" domain: example.com"));
|
|
979
1082
|
console.log(chalk_1.default.cyan("\n # Or connect to a remote server:"));
|
|
980
1083
|
console.log(chalk_1.default.white(" server:"));
|
|
981
|
-
console.log(chalk_1.default.white(" remote:
|
|
1084
|
+
console.log(chalk_1.default.white(" remote: example.com"));
|
|
982
1085
|
process.exit(1);
|
|
983
1086
|
}
|
|
984
1087
|
if (isClientMode) {
|
|
985
1088
|
// CLIENT MODE: Connect to remote server
|
|
986
|
-
|
|
987
|
-
const serverUrl =
|
|
1089
|
+
// Build URL using basePath (default: op) -> wss://op.example.com/_tunnel
|
|
1090
|
+
const { url: serverUrl } = buildServerUrl(remote, !useHttps, basePath);
|
|
988
1091
|
console.log(chalk_1.default.cyan(`Connecting to ${remote}...\n`));
|
|
989
1092
|
console.log(chalk_1.default.cyan(`Starting ${tunnelsToStart.length} tunnel(s)...\n`));
|
|
990
1093
|
try {
|
|
@@ -1040,10 +1143,10 @@ program
|
|
|
1040
1143
|
}
|
|
1041
1144
|
}
|
|
1042
1145
|
});
|
|
1043
|
-
// Down command - stop all tunnels
|
|
1146
|
+
// Down command - stop all tunnels
|
|
1044
1147
|
program
|
|
1045
1148
|
.command("down")
|
|
1046
|
-
.description("Stop all running tunnels
|
|
1149
|
+
.description("Stop all running tunnels")
|
|
1047
1150
|
.action(async () => {
|
|
1048
1151
|
const fs = await Promise.resolve().then(() => __importStar(require("fs")));
|
|
1049
1152
|
const path = await Promise.resolve().then(() => __importStar(require("path")));
|
|
@@ -1087,10 +1190,10 @@ program
|
|
|
1087
1190
|
}
|
|
1088
1191
|
console.log(chalk_1.default.green("\nAll tunnels stopped"));
|
|
1089
1192
|
});
|
|
1090
|
-
// PS command - list running tunnel processes
|
|
1193
|
+
// PS command - list running tunnel processes
|
|
1091
1194
|
program
|
|
1092
1195
|
.command("ps")
|
|
1093
|
-
.description("List running tunnel processes
|
|
1196
|
+
.description("List running tunnel processes")
|
|
1094
1197
|
.action(async () => {
|
|
1095
1198
|
const fs = await Promise.resolve().then(() => __importStar(require("fs")));
|
|
1096
1199
|
const path = await Promise.resolve().then(() => __importStar(require("path")));
|