opentunnel-cli 1.0.5 → 1.0.7
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 +142 -76
- package/dist/cli/index.js +272 -151
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -44,34 +44,104 @@ 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");
|
|
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) {
|
|
113
|
+
let hostname = server;
|
|
114
|
+
// Remove protocol if provided
|
|
115
|
+
hostname = hostname.replace(/^(wss?|https?):\/\//, "");
|
|
116
|
+
// Remove trailing path
|
|
117
|
+
hostname = hostname.replace(/\/_tunnel.*$/, "");
|
|
118
|
+
// Remove trailing slash
|
|
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;
|
|
125
|
+
const protocol = insecure ? "ws" : "wss";
|
|
126
|
+
return {
|
|
127
|
+
url: `${protocol}://${fullHostname}/_tunnel`,
|
|
128
|
+
displayName: hostname,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
54
131
|
// Quick command - quick tunnel to any server
|
|
55
132
|
program
|
|
56
133
|
.command("quick <port>")
|
|
57
134
|
.description("Instantly expose a local port to the internet")
|
|
58
|
-
.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)")
|
|
59
137
|
.option("-n, --subdomain <name>", "Request a specific subdomain (e.g., 'myapp')")
|
|
60
138
|
.option("-p, --protocol <proto>", "Protocol (http, https, tcp)", "http")
|
|
61
139
|
.option("-h, --host <host>", "Local host to forward to", "localhost")
|
|
62
140
|
.option("-t, --token <token>", "Authentication token (if server requires it)")
|
|
63
141
|
.option("--insecure", "Skip SSL certificate verification (for self-signed certs)")
|
|
64
142
|
.action(async (port, options) => {
|
|
65
|
-
//
|
|
66
|
-
const serverUrl = options.server;
|
|
67
|
-
let serverDisplayName = serverUrl;
|
|
68
|
-
try {
|
|
69
|
-
const url = new URL(serverUrl.replace("wss://", "https://").replace("ws://", "http://"));
|
|
70
|
-
serverDisplayName = url.hostname;
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
// Keep original if parsing fails
|
|
74
|
-
}
|
|
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);
|
|
75
145
|
console.log(chalk_1.default.cyan(`
|
|
76
146
|
██████╗ ██████╗ ███████╗███╗ ██╗████████╗██╗ ██╗███╗ ██╗███╗ ██╗███████╗██╗
|
|
77
147
|
██╔═══██╗██╔══██╗██╔════╝████╗ ██║╚══██╔══╝██║ ██║████╗ ██║████╗ ██║██╔════╝██║
|
|
@@ -151,15 +221,16 @@ program
|
|
|
151
221
|
program
|
|
152
222
|
.command("http <port>")
|
|
153
223
|
.description("Expose a local HTTP server")
|
|
154
|
-
.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)")
|
|
155
226
|
.option("-t, --token <token>", "Authentication token")
|
|
156
227
|
.option("-n, --subdomain <name>", "Custom subdomain (e.g., 'myapp' for myapp.op.domain.com)")
|
|
157
228
|
.option("-d, --detach", "Run tunnel in background")
|
|
158
229
|
.option("-h, --host <host>", "Local host", "localhost")
|
|
159
230
|
.option("--domain <domain>", "Domain for the tunnel", "localhost")
|
|
160
231
|
.option("--port <port>", "Server port", "443")
|
|
161
|
-
.option("--base-path <path>", "Subdomain base path", "op")
|
|
162
232
|
.option("--https", "Use HTTPS for local connection")
|
|
233
|
+
.option("--insecure", "Skip SSL verification (for self-signed certs)")
|
|
163
234
|
.option("--ngrok", "Use ngrok instead of OpenTunnel server")
|
|
164
235
|
.option("--region <region>", "Ngrok region (us, eu, ap, au, sa, jp, in)", "us")
|
|
165
236
|
.action(async (port, options) => {
|
|
@@ -174,16 +245,17 @@ program
|
|
|
174
245
|
});
|
|
175
246
|
return;
|
|
176
247
|
}
|
|
177
|
-
// If remote server
|
|
248
|
+
// If remote server domain provided, just connect to it
|
|
178
249
|
if (options.server) {
|
|
250
|
+
const { url: serverUrl } = buildServerUrl(options.server, options.insecure, options.basePath);
|
|
179
251
|
await createTunnel({
|
|
180
252
|
protocol: options.https ? "https" : "http",
|
|
181
253
|
localHost: options.host,
|
|
182
254
|
localPort: parseInt(port),
|
|
183
255
|
subdomain: options.subdomain,
|
|
184
|
-
serverUrl
|
|
256
|
+
serverUrl,
|
|
185
257
|
token: options.token,
|
|
186
|
-
insecure:
|
|
258
|
+
insecure: options.insecure,
|
|
187
259
|
});
|
|
188
260
|
return;
|
|
189
261
|
}
|
|
@@ -236,12 +308,14 @@ program
|
|
|
236
308
|
program
|
|
237
309
|
.command("tcp <port>")
|
|
238
310
|
.description("Expose a local TCP server")
|
|
239
|
-
.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)")
|
|
240
313
|
.option("-t, --token <token>", "Authentication token")
|
|
241
314
|
.option("-r, --remote-port <port>", "Remote port to use")
|
|
242
315
|
.option("-h, --host <host>", "Local host", "localhost")
|
|
243
316
|
.option("--domain <domain>", "Domain for the tunnel", "localhost")
|
|
244
317
|
.option("--port <port>", "Server port", "443")
|
|
318
|
+
.option("--insecure", "Skip SSL verification (for self-signed certs)")
|
|
245
319
|
.option("--ngrok", "Use ngrok instead of OpenTunnel server")
|
|
246
320
|
.option("--region <region>", "Ngrok region (us, eu, ap, au, sa, jp, in)", "us")
|
|
247
321
|
.action(async (port, options) => {
|
|
@@ -256,16 +330,17 @@ program
|
|
|
256
330
|
});
|
|
257
331
|
return;
|
|
258
332
|
}
|
|
259
|
-
// If remote server
|
|
333
|
+
// If remote server domain provided, just connect to it
|
|
260
334
|
if (options.server) {
|
|
335
|
+
const { url: serverUrl } = buildServerUrl(options.server, options.insecure, options.basePath);
|
|
261
336
|
await createTunnel({
|
|
262
337
|
protocol: "tcp",
|
|
263
338
|
localHost: options.host,
|
|
264
339
|
localPort: parseInt(port),
|
|
265
340
|
remotePort: options.remotePort ? parseInt(options.remotePort) : undefined,
|
|
266
|
-
serverUrl
|
|
341
|
+
serverUrl,
|
|
267
342
|
token: options.token,
|
|
268
|
-
insecure:
|
|
343
|
+
insecure: options.insecure,
|
|
269
344
|
});
|
|
270
345
|
return;
|
|
271
346
|
}
|
|
@@ -356,13 +431,13 @@ program
|
|
|
356
431
|
program
|
|
357
432
|
.command("server")
|
|
358
433
|
.description("Start the OpenTunnel server (standalone mode)")
|
|
359
|
-
.option("-p, --port <port>", "Server port"
|
|
434
|
+
.option("-p, --port <port>", "Server port")
|
|
360
435
|
.option("--public-port <port>", "Public port shown in URLs (default: same as port)")
|
|
361
|
-
.option("--domain <domain>", "Base domain"
|
|
362
|
-
.option("-b, --base-path <path>", "Subdomain base path (e.g., 'op' for *.op.domain.com)"
|
|
363
|
-
.option("--host <host>", "Bind host"
|
|
364
|
-
.option("--tcp-min <port>", "Minimum TCP port"
|
|
365
|
-
.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")
|
|
366
441
|
.option("--auth-tokens <tokens>", "Comma-separated auth tokens")
|
|
367
442
|
.option("--no-https", "Disable HTTPS (use plain HTTP)")
|
|
368
443
|
.option("--https-cert <path>", "Path to SSL certificate (for custom certs)")
|
|
@@ -374,16 +449,48 @@ program
|
|
|
374
449
|
.option("--duckdns-token <token>", "DuckDNS token for dynamic DNS updates")
|
|
375
450
|
.option("-d, --detach", "Run server in background (detached mode)")
|
|
376
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
|
+
};
|
|
377
484
|
// Detached mode - run in background
|
|
378
|
-
if (
|
|
485
|
+
if (mergedOptions.detach) {
|
|
379
486
|
const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
|
|
380
|
-
const
|
|
381
|
-
const
|
|
382
|
-
const pidFile =
|
|
383
|
-
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");
|
|
384
491
|
// Check if already running
|
|
385
|
-
if (
|
|
386
|
-
const oldPid =
|
|
492
|
+
if (fsAsync.existsSync(pidFile)) {
|
|
493
|
+
const oldPid = fsAsync.readFileSync(pidFile, "utf-8").trim();
|
|
387
494
|
try {
|
|
388
495
|
process.kill(parseInt(oldPid), 0);
|
|
389
496
|
console.log(chalk_1.default.yellow(`Server already running (PID: ${oldPid})`));
|
|
@@ -391,60 +498,48 @@ program
|
|
|
391
498
|
return;
|
|
392
499
|
}
|
|
393
500
|
catch {
|
|
394
|
-
|
|
501
|
+
fsAsync.unlinkSync(pidFile);
|
|
395
502
|
}
|
|
396
503
|
}
|
|
397
|
-
// Build args without -d flag
|
|
504
|
+
// Build args without -d flag, using merged options
|
|
398
505
|
const args = ["server"];
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
if (
|
|
406
|
-
args.push("-
|
|
407
|
-
if (
|
|
408
|
-
args.push("--
|
|
409
|
-
if (
|
|
410
|
-
args.push("--tcp-min", options.tcpMin);
|
|
411
|
-
if (options.tcpMax)
|
|
412
|
-
args.push("--tcp-max", options.tcpMax);
|
|
413
|
-
if (options.authTokens)
|
|
414
|
-
args.push("--auth-tokens", options.authTokens);
|
|
415
|
-
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)
|
|
416
517
|
args.push("--https");
|
|
417
|
-
if (
|
|
418
|
-
args.push("--email",
|
|
419
|
-
if (
|
|
518
|
+
if (mergedOptions.email)
|
|
519
|
+
args.push("--email", mergedOptions.email);
|
|
520
|
+
if (mergedOptions.production)
|
|
420
521
|
args.push("--production");
|
|
421
|
-
if (
|
|
422
|
-
args.push("--cloudflare-token",
|
|
423
|
-
if (
|
|
424
|
-
args.push("--duckdns-token",
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
if (options.dnsCreateRecords)
|
|
428
|
-
args.push("--dns-create-records");
|
|
429
|
-
if (options.dnsDeleteOnClose)
|
|
430
|
-
args.push("--dns-delete-on-close");
|
|
431
|
-
const out = fs.openSync(logFile, "a");
|
|
432
|
-
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");
|
|
433
528
|
const child = spawn(process.execPath, [process.argv[1], ...args], {
|
|
434
529
|
detached: true,
|
|
435
530
|
stdio: ["ignore", out, err],
|
|
436
531
|
cwd: process.cwd(),
|
|
437
532
|
});
|
|
438
533
|
child.unref();
|
|
439
|
-
|
|
534
|
+
fsAsync.writeFileSync(pidFile, String(child.pid));
|
|
440
535
|
console.log(chalk_1.default.green(`OpenTunnel server started in background`));
|
|
441
536
|
console.log(chalk_1.default.gray(` PID: ${child.pid}`));
|
|
442
|
-
console.log(chalk_1.default.gray(` Port: ${
|
|
443
|
-
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}`));
|
|
444
539
|
console.log(chalk_1.default.gray(` Log: ${logFile}`));
|
|
445
540
|
console.log(chalk_1.default.gray(` PID file: ${pidFile}`));
|
|
446
541
|
console.log("");
|
|
447
|
-
console.log(chalk_1.default.gray(`Stop with:
|
|
542
|
+
console.log(chalk_1.default.gray(`Stop with: opentunnel stop`));
|
|
448
543
|
console.log(chalk_1.default.gray(`Logs: tail -f ${logFile}`));
|
|
449
544
|
return;
|
|
450
545
|
}
|
|
@@ -454,46 +549,46 @@ program
|
|
|
454
549
|
let httpsConfig = undefined;
|
|
455
550
|
let selfSignedHttpsConfig = undefined;
|
|
456
551
|
let autoHttpsConfig = undefined;
|
|
457
|
-
if (
|
|
552
|
+
if (mergedOptions.httpsCert && mergedOptions.httpsKey) {
|
|
458
553
|
// Custom certificates provided
|
|
459
|
-
const
|
|
554
|
+
const fsRead = await Promise.resolve().then(() => __importStar(require("fs")));
|
|
460
555
|
httpsConfig = {
|
|
461
|
-
cert:
|
|
462
|
-
key:
|
|
556
|
+
cert: fsRead.readFileSync(mergedOptions.httpsCert, "utf-8"),
|
|
557
|
+
key: fsRead.readFileSync(mergedOptions.httpsKey, "utf-8"),
|
|
463
558
|
};
|
|
464
559
|
}
|
|
465
|
-
else if (
|
|
560
|
+
else if (mergedOptions.letsencrypt) {
|
|
466
561
|
// Let's Encrypt
|
|
467
562
|
autoHttpsConfig = {
|
|
468
563
|
enabled: true,
|
|
469
|
-
email:
|
|
470
|
-
production:
|
|
471
|
-
cloudflareToken:
|
|
564
|
+
email: mergedOptions.email || `admin@${mergedOptions.domain}`,
|
|
565
|
+
production: mergedOptions.production || false,
|
|
566
|
+
cloudflareToken: mergedOptions.cloudflareToken,
|
|
472
567
|
};
|
|
473
568
|
}
|
|
474
569
|
else {
|
|
475
570
|
// Self-signed by default (use --no-https to disable)
|
|
476
571
|
selfSignedHttpsConfig = {
|
|
477
|
-
enabled:
|
|
572
|
+
enabled: mergedOptions.https !== false,
|
|
478
573
|
};
|
|
479
574
|
}
|
|
480
575
|
const server = new TunnelServer({
|
|
481
|
-
port: parseInt(
|
|
482
|
-
publicPort:
|
|
483
|
-
host:
|
|
484
|
-
domain:
|
|
485
|
-
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,
|
|
486
581
|
tunnelPortRange: {
|
|
487
|
-
min: parseInt(
|
|
488
|
-
max: parseInt(
|
|
582
|
+
min: parseInt(mergedOptions.tcpMin),
|
|
583
|
+
max: parseInt(mergedOptions.tcpMax),
|
|
489
584
|
},
|
|
490
|
-
auth:
|
|
491
|
-
? { required: true, tokens:
|
|
585
|
+
auth: mergedOptions.authTokens
|
|
586
|
+
? { required: true, tokens: mergedOptions.authTokens.split(",") }
|
|
492
587
|
: undefined,
|
|
493
588
|
https: httpsConfig,
|
|
494
589
|
selfSignedHttps: selfSignedHttpsConfig,
|
|
495
590
|
autoHttps: autoHttpsConfig,
|
|
496
|
-
autoDns: detectDnsConfig(
|
|
591
|
+
autoDns: detectDnsConfig(mergedOptions),
|
|
497
592
|
});
|
|
498
593
|
// Helper function to auto-detect DNS provider
|
|
499
594
|
function detectDnsConfig(opts) {
|
|
@@ -506,8 +601,8 @@ program
|
|
|
506
601
|
enabled: true,
|
|
507
602
|
provider: "cloudflare",
|
|
508
603
|
cloudflareToken: opts.cloudflareToken,
|
|
509
|
-
createRecords:
|
|
510
|
-
deleteOnClose:
|
|
604
|
+
createRecords: false,
|
|
605
|
+
deleteOnClose: false,
|
|
511
606
|
setupWildcard: true,
|
|
512
607
|
};
|
|
513
608
|
}
|
|
@@ -516,15 +611,11 @@ program
|
|
|
516
611
|
enabled: true,
|
|
517
612
|
provider: "duckdns",
|
|
518
613
|
duckdnsToken: opts.duckdnsToken,
|
|
519
|
-
createRecords: false,
|
|
614
|
+
createRecords: false,
|
|
520
615
|
deleteOnClose: false,
|
|
521
616
|
setupWildcard: false,
|
|
522
617
|
};
|
|
523
618
|
}
|
|
524
|
-
// No auto DNS if no tokens provided
|
|
525
|
-
if (opts.autoDns) {
|
|
526
|
-
console.log(chalk_1.default.yellow("Warning: --auto-dns requires --cloudflare-token or --duckdns-token"));
|
|
527
|
-
}
|
|
528
619
|
return undefined;
|
|
529
620
|
}
|
|
530
621
|
console.log(chalk_1.default.cyan(`
|
|
@@ -542,10 +633,10 @@ program
|
|
|
542
633
|
console.log(chalk_1.default.yellow(`[-] Tunnel closed: ${tunnelId}`));
|
|
543
634
|
});
|
|
544
635
|
await server.start();
|
|
545
|
-
console.log(chalk_1.default.green(`\nServer running on ${
|
|
546
|
-
console.log(chalk_1.default.gray(`Domain: ${
|
|
547
|
-
console.log(chalk_1.default.gray(`Subdomain pattern: *.${
|
|
548
|
-
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`));
|
|
549
640
|
});
|
|
550
641
|
// Stop command
|
|
551
642
|
program
|
|
@@ -868,48 +959,82 @@ program
|
|
|
868
959
|
.command("init")
|
|
869
960
|
.description("Create an example opentunnel.yml configuration file")
|
|
870
961
|
.option("-f, --force", "Overwrite existing config file")
|
|
962
|
+
.option("--server", "Create server mode config")
|
|
963
|
+
.option("--client", "Create client mode config (default)")
|
|
871
964
|
.action(async (options) => {
|
|
872
|
-
const
|
|
873
|
-
const
|
|
874
|
-
const configPath =
|
|
875
|
-
|
|
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) {
|
|
876
970
|
console.log(chalk_1.default.yellow(`Config file already exists: ${configPath}`));
|
|
877
971
|
console.log(chalk_1.default.gray("Use --force to overwrite"));
|
|
878
972
|
return;
|
|
879
973
|
}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
974
|
+
// Example config with environment variable syntax
|
|
975
|
+
const clientConfig = `# OpenTunnel Client Configuration
|
|
976
|
+
# Supports environment variables: \${VAR} or \${VAR:-default}
|
|
977
|
+
# Create a .env file for secrets (automatically loaded)
|
|
978
|
+
|
|
979
|
+
version: "1.0"
|
|
980
|
+
|
|
981
|
+
server:
|
|
982
|
+
remote: \${SERVER_DOMAIN:-example.com} # Server domain (system adds basePath)
|
|
983
|
+
token: \${AUTH_TOKEN} # From .env (required for private servers)
|
|
984
|
+
|
|
985
|
+
tunnels:
|
|
986
|
+
- name: web
|
|
987
|
+
protocol: http
|
|
988
|
+
port: 3000
|
|
989
|
+
subdomain: web # → web.op.example.com
|
|
990
|
+
autostart: true
|
|
991
|
+
|
|
992
|
+
- name: api
|
|
993
|
+
protocol: http
|
|
994
|
+
port: 4000
|
|
995
|
+
subdomain: api # → api.op.example.com
|
|
996
|
+
|
|
997
|
+
- name: database
|
|
998
|
+
protocol: tcp
|
|
999
|
+
port: 5432
|
|
1000
|
+
remotePort: 15432 # → example.com:15432
|
|
1001
|
+
autostart: false
|
|
1002
|
+
`;
|
|
1003
|
+
const serverConfig = `# OpenTunnel Server Configuration
|
|
1004
|
+
# Supports environment variables: \${VAR} or \${VAR:-default}
|
|
1005
|
+
# Create a .env file for secrets (automatically loaded)
|
|
1006
|
+
|
|
1007
|
+
version: "1.0"
|
|
1008
|
+
|
|
1009
|
+
server:
|
|
1010
|
+
domain: \${DOMAIN:-example.com} # Your base domain
|
|
1011
|
+
token: \${AUTH_TOKEN} # From .env (optional for public server)
|
|
1012
|
+
|
|
1013
|
+
tunnels: []
|
|
1014
|
+
`;
|
|
1015
|
+
const envExample = `# OpenTunnel Environment Variables
|
|
1016
|
+
# Copy to .env and fill in your values
|
|
1017
|
+
|
|
1018
|
+
# Server/Remote domain
|
|
1019
|
+
DOMAIN=example.com
|
|
1020
|
+
SERVER_DOMAIN=example.com
|
|
1021
|
+
|
|
1022
|
+
# Authentication token (leave empty for public server)
|
|
1023
|
+
AUTH_TOKEN=
|
|
1024
|
+
`;
|
|
1025
|
+
const configContent = options.server ? serverConfig : clientConfig;
|
|
1026
|
+
fsInit.writeFileSync(configPath, configContent);
|
|
1027
|
+
// Create .env.example if it doesn't exist
|
|
1028
|
+
const envExamplePath = pathInit.join(process.cwd(), ".env.example");
|
|
1029
|
+
if (!fsInit.existsSync(envExamplePath) && !fsInit.existsSync(envPath)) {
|
|
1030
|
+
fsInit.writeFileSync(envExamplePath, envExample);
|
|
1031
|
+
console.log(chalk_1.default.green(`Created .env.example`));
|
|
1032
|
+
}
|
|
911
1033
|
console.log(chalk_1.default.green(`Created ${CONFIG_FILE}`));
|
|
912
|
-
console.log(chalk_1.default.gray(`\
|
|
1034
|
+
console.log(chalk_1.default.gray(`\nEnvironment variables supported:`));
|
|
1035
|
+
console.log(chalk_1.default.cyan(` \${VAR} → Use value of VAR`));
|
|
1036
|
+
console.log(chalk_1.default.cyan(` \${VAR:-default} → Use VAR or "default" if not set`));
|
|
1037
|
+
console.log(chalk_1.default.gray(`\nCreate a .env file for secrets, then run:`));
|
|
913
1038
|
console.log(chalk_1.default.cyan(` opentunnel up # Start all tunnels`));
|
|
914
1039
|
console.log(chalk_1.default.cyan(` opentunnel up -d # Start in background`));
|
|
915
1040
|
});
|
|
@@ -921,16 +1046,12 @@ program
|
|
|
921
1046
|
.option("-f, --file <path>", "Config file path", CONFIG_FILE)
|
|
922
1047
|
.option("--no-autostart", "Ignore autostart setting, start all tunnels")
|
|
923
1048
|
.action(async (options) => {
|
|
924
|
-
const
|
|
925
|
-
const
|
|
926
|
-
// Load config file
|
|
927
|
-
const configPath =
|
|
928
|
-
|
|
929
|
-
if (
|
|
930
|
-
const configContent = fs.readFileSync(configPath, "utf-8");
|
|
931
|
-
config = (0, yaml_1.parse)(configContent);
|
|
932
|
-
}
|
|
933
|
-
else {
|
|
1049
|
+
const fsModule = await Promise.resolve().then(() => __importStar(require("fs")));
|
|
1050
|
+
const pathModule = await Promise.resolve().then(() => __importStar(require("path")));
|
|
1051
|
+
// Load config file (with env variable substitution)
|
|
1052
|
+
const configPath = pathModule.join(process.cwd(), options.file);
|
|
1053
|
+
const config = loadConfig(configPath);
|
|
1054
|
+
if (!config) {
|
|
934
1055
|
console.log(chalk_1.default.red(`Config file not found: ${configPath}`));
|
|
935
1056
|
console.log(chalk_1.default.gray(`Run 'opentunnel init' to create one`));
|
|
936
1057
|
return;
|
|
@@ -964,16 +1085,16 @@ program
|
|
|
964
1085
|
console.log(chalk_1.default.gray("\nAdd to your config:"));
|
|
965
1086
|
console.log(chalk_1.default.cyan("\n # Run your own server:"));
|
|
966
1087
|
console.log(chalk_1.default.white(" server:"));
|
|
967
|
-
console.log(chalk_1.default.white(" domain:
|
|
1088
|
+
console.log(chalk_1.default.white(" domain: example.com"));
|
|
968
1089
|
console.log(chalk_1.default.cyan("\n # Or connect to a remote server:"));
|
|
969
1090
|
console.log(chalk_1.default.white(" server:"));
|
|
970
|
-
console.log(chalk_1.default.white(" remote:
|
|
1091
|
+
console.log(chalk_1.default.white(" remote: example.com"));
|
|
971
1092
|
process.exit(1);
|
|
972
1093
|
}
|
|
973
1094
|
if (isClientMode) {
|
|
974
1095
|
// CLIENT MODE: Connect to remote server
|
|
975
|
-
|
|
976
|
-
const serverUrl =
|
|
1096
|
+
// Build URL using basePath (default: op) -> wss://op.example.com/_tunnel
|
|
1097
|
+
const { url: serverUrl } = buildServerUrl(remote, !useHttps, basePath);
|
|
977
1098
|
console.log(chalk_1.default.cyan(`Connecting to ${remote}...\n`));
|
|
978
1099
|
console.log(chalk_1.default.cyan(`Starting ${tunnelsToStart.length} tunnel(s)...\n`));
|
|
979
1100
|
try {
|