opentunnel-cli 1.0.0
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/LICENSE +21 -0
- package/README.md +284 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +1357 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/client/NgrokClient.d.ts +40 -0
- package/dist/client/NgrokClient.d.ts.map +1 -0
- package/dist/client/NgrokClient.js +155 -0
- package/dist/client/NgrokClient.js.map +1 -0
- package/dist/client/TunnelClient.d.ts +47 -0
- package/dist/client/TunnelClient.d.ts.map +1 -0
- package/dist/client/TunnelClient.js +435 -0
- package/dist/client/TunnelClient.js.map +1 -0
- package/dist/client/index.d.ts +3 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +8 -0
- package/dist/client/index.js.map +1 -0
- package/dist/dns/CloudflareDNS.d.ts +45 -0
- package/dist/dns/CloudflareDNS.d.ts.map +1 -0
- package/dist/dns/CloudflareDNS.js +286 -0
- package/dist/dns/CloudflareDNS.js.map +1 -0
- package/dist/dns/DuckDNS.d.ts +20 -0
- package/dist/dns/DuckDNS.d.ts.map +1 -0
- package/dist/dns/DuckDNS.js +109 -0
- package/dist/dns/DuckDNS.js.map +1 -0
- package/dist/dns/index.d.ts +3 -0
- package/dist/dns/index.d.ts.map +1 -0
- package/dist/dns/index.js +9 -0
- package/dist/dns/index.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/server/CertManager.d.ts +54 -0
- package/dist/server/CertManager.d.ts.map +1 -0
- package/dist/server/CertManager.js +414 -0
- package/dist/server/CertManager.js.map +1 -0
- package/dist/server/TunnelServer.d.ts +42 -0
- package/dist/server/TunnelServer.d.ts.map +1 -0
- package/dist/server/TunnelServer.js +790 -0
- package/dist/server/TunnelServer.js.map +1 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +48 -0
- package/dist/server/index.js.map +1 -0
- package/dist/shared/types.d.ts +147 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +3 -0
- package/dist/shared/types.js.map +1 -0
- package/dist/shared/utils.d.ts +29 -0
- package/dist/shared/utils.d.ts.map +1 -0
- package/dist/shared/utils.js +135 -0
- package/dist/shared/utils.js.map +1 -0
- package/package.json +66 -0
|
@@ -0,0 +1,1357 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
37
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
38
|
+
};
|
|
39
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
const commander_1 = require("commander");
|
|
41
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
42
|
+
const ora_1 = __importDefault(require("ora"));
|
|
43
|
+
const TunnelClient_1 = require("../client/TunnelClient");
|
|
44
|
+
const NgrokClient_1 = require("../client/NgrokClient");
|
|
45
|
+
const utils_1 = require("../shared/utils");
|
|
46
|
+
const yaml_1 = require("yaml");
|
|
47
|
+
const CONFIG_FILE = "opentunnel.yml";
|
|
48
|
+
const program = new commander_1.Command();
|
|
49
|
+
program
|
|
50
|
+
.name("opentunnel")
|
|
51
|
+
.alias("ot")
|
|
52
|
+
.description("Expose local ports to the internet via custom domains or ngrok")
|
|
53
|
+
.version("1.0.0");
|
|
54
|
+
// HTTP tunnel command
|
|
55
|
+
program
|
|
56
|
+
.command("http <port>")
|
|
57
|
+
.description("Expose a local HTTP server")
|
|
58
|
+
.option("-s, --server <url>", "Server URL (use 'ngrok' for ngrok)")
|
|
59
|
+
.option("-t, --token <token>", "Authentication token (or ngrok authtoken)")
|
|
60
|
+
.option("-n, --subdomain <name>", "Custom subdomain (e.g., 'myapp' for myapp.op.domain.com)")
|
|
61
|
+
.option("-d, --detach", "Run tunnel in background")
|
|
62
|
+
.option("-h, --host <host>", "Local host", "localhost")
|
|
63
|
+
.option("--domain <domain>", "Server domain (e.g., domain.com)")
|
|
64
|
+
.option("--https", "Use HTTPS for local connection")
|
|
65
|
+
.option("--insecure", "Skip SSL certificate verification (for self-signed certs)")
|
|
66
|
+
.option("--ngrok", "Use ngrok instead of OpenTunnel server")
|
|
67
|
+
.option("--region <region>", "Ngrok region (us, eu, ap, au, sa, jp, in)", "us")
|
|
68
|
+
.action(async (port, options) => {
|
|
69
|
+
// Build server URL from domain if provided
|
|
70
|
+
const serverUrl = options.server || (options.domain
|
|
71
|
+
? `wss://${options.domain}/_tunnel`
|
|
72
|
+
: "ws://localhost:8080/_tunnel");
|
|
73
|
+
if (options.detach) {
|
|
74
|
+
await runTunnelInBackground("http", port, { ...options, server: serverUrl });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (options.ngrok || options.server === "ngrok") {
|
|
78
|
+
await createNgrokTunnel({
|
|
79
|
+
protocol: options.https ? "https" : "http",
|
|
80
|
+
localHost: options.host,
|
|
81
|
+
localPort: parseInt(port),
|
|
82
|
+
subdomain: options.subdomain,
|
|
83
|
+
authtoken: options.token,
|
|
84
|
+
region: options.region,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
await createTunnel({
|
|
89
|
+
protocol: options.https ? "https" : "http",
|
|
90
|
+
localHost: options.host,
|
|
91
|
+
localPort: parseInt(port),
|
|
92
|
+
subdomain: options.subdomain,
|
|
93
|
+
serverUrl,
|
|
94
|
+
token: options.token,
|
|
95
|
+
insecure: options.insecure,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
// TCP tunnel command
|
|
100
|
+
program
|
|
101
|
+
.command("tcp <port>")
|
|
102
|
+
.description("Expose a local TCP server")
|
|
103
|
+
.option("-s, --server <url>", "Server URL (use 'ngrok' for ngrok)")
|
|
104
|
+
.option("-t, --token <token>", "Authentication token (or ngrok authtoken)")
|
|
105
|
+
.option("-r, --remote-port <port>", "Remote port to use")
|
|
106
|
+
.option("-h, --host <host>", "Local host", "localhost")
|
|
107
|
+
.option("-d, --detach", "Run tunnel in background")
|
|
108
|
+
.option("--domain <domain>", "Server domain (e.g., domain.com)")
|
|
109
|
+
.option("--insecure", "Skip SSL certificate verification (for self-signed certs)")
|
|
110
|
+
.option("--ngrok", "Use ngrok instead of OpenTunnel server")
|
|
111
|
+
.option("--region <region>", "Ngrok region (us, eu, ap, au, sa, jp, in)", "us")
|
|
112
|
+
.action(async (port, options) => {
|
|
113
|
+
const serverUrl = options.server || (options.domain
|
|
114
|
+
? `wss://${options.domain}/_tunnel`
|
|
115
|
+
: "ws://localhost:8080/_tunnel");
|
|
116
|
+
if (options.detach) {
|
|
117
|
+
await runTunnelInBackground("tcp", port, { ...options, server: serverUrl });
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (options.ngrok || options.server === "ngrok") {
|
|
121
|
+
await createNgrokTunnel({
|
|
122
|
+
protocol: "tcp",
|
|
123
|
+
localHost: options.host,
|
|
124
|
+
localPort: parseInt(port),
|
|
125
|
+
remotePort: options.remotePort ? parseInt(options.remotePort) : undefined,
|
|
126
|
+
authtoken: options.token,
|
|
127
|
+
region: options.region,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
await createTunnel({
|
|
132
|
+
protocol: "tcp",
|
|
133
|
+
localHost: options.host,
|
|
134
|
+
localPort: parseInt(port),
|
|
135
|
+
remotePort: options.remotePort ? parseInt(options.remotePort) : undefined,
|
|
136
|
+
serverUrl,
|
|
137
|
+
token: options.token,
|
|
138
|
+
insecure: options.insecure,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
// Quick expose command
|
|
143
|
+
program
|
|
144
|
+
.command("expose <port>")
|
|
145
|
+
.description("Quick expose a local port (auto-detects HTTP)")
|
|
146
|
+
.option("-s, --server <url>", "Server URL")
|
|
147
|
+
.option("-t, --token <token>", "Authentication token")
|
|
148
|
+
.option("-n, --subdomain <name>", "Custom subdomain")
|
|
149
|
+
.option("-d, --detach", "Run tunnel in background")
|
|
150
|
+
.option("-p, --protocol <proto>", "Protocol (http, https, tcp)", "http")
|
|
151
|
+
.option("--domain <domain>", "Server domain (e.g., domain.com)")
|
|
152
|
+
.option("--insecure", "Skip SSL certificate verification (for self-signed certs)")
|
|
153
|
+
.option("--ngrok", "Use ngrok instead of OpenTunnel server")
|
|
154
|
+
.action(async (port, options) => {
|
|
155
|
+
const serverUrl = options.server || (options.domain
|
|
156
|
+
? `wss://${options.domain}/_tunnel`
|
|
157
|
+
: "ws://localhost:8080/_tunnel");
|
|
158
|
+
if (options.detach) {
|
|
159
|
+
await runTunnelInBackground("expose", port, { ...options, server: serverUrl });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (options.ngrok || options.server === "ngrok") {
|
|
163
|
+
await createNgrokTunnel({
|
|
164
|
+
protocol: options.protocol,
|
|
165
|
+
localHost: "localhost",
|
|
166
|
+
localPort: parseInt(port),
|
|
167
|
+
subdomain: options.subdomain,
|
|
168
|
+
authtoken: options.token,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
await createTunnel({
|
|
173
|
+
protocol: options.protocol,
|
|
174
|
+
localHost: "localhost",
|
|
175
|
+
localPort: parseInt(port),
|
|
176
|
+
subdomain: options.subdomain,
|
|
177
|
+
serverUrl,
|
|
178
|
+
token: options.token,
|
|
179
|
+
insecure: options.insecure,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
// Server command
|
|
184
|
+
program
|
|
185
|
+
.command("server")
|
|
186
|
+
.description("Start the OpenTunnel server (standalone mode)")
|
|
187
|
+
.option("-p, --port <port>", "Server port", "443")
|
|
188
|
+
.option("--public-port <port>", "Public port shown in URLs (default: same as port)")
|
|
189
|
+
.option("--domain <domain>", "Base domain", "localhost")
|
|
190
|
+
.option("-b, --base-path <path>", "Subdomain base path (e.g., 'op' for *.op.domain.com)", "op")
|
|
191
|
+
.option("--host <host>", "Bind host", "0.0.0.0")
|
|
192
|
+
.option("--tcp-min <port>", "Minimum TCP port", "10000")
|
|
193
|
+
.option("--tcp-max <port>", "Maximum TCP port", "20000")
|
|
194
|
+
.option("--auth-tokens <tokens>", "Comma-separated auth tokens")
|
|
195
|
+
.option("--no-https", "Disable HTTPS (use plain HTTP)")
|
|
196
|
+
.option("--https-cert <path>", "Path to SSL certificate (for custom certs)")
|
|
197
|
+
.option("--https-key <path>", "Path to SSL private key (for custom certs)")
|
|
198
|
+
.option("--letsencrypt", "Use Let's Encrypt instead of self-signed (requires port 80)")
|
|
199
|
+
.option("--email <email>", "Email for Let's Encrypt notifications")
|
|
200
|
+
.option("--production", "Use Let's Encrypt production (default: staging)")
|
|
201
|
+
.option("--cloudflare-token <token>", "Cloudflare API token for DNS-01 challenge")
|
|
202
|
+
.option("--duckdns-token <token>", "DuckDNS token for dynamic DNS updates")
|
|
203
|
+
.option("-d, --detach", "Run server in background (detached mode)")
|
|
204
|
+
.action(async (options) => {
|
|
205
|
+
// Detached mode - run in background
|
|
206
|
+
if (options.detach) {
|
|
207
|
+
const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
|
|
208
|
+
const fs = await Promise.resolve().then(() => __importStar(require("fs")));
|
|
209
|
+
const path = await Promise.resolve().then(() => __importStar(require("path")));
|
|
210
|
+
const pidFile = path.join(process.cwd(), ".opentunnel.pid");
|
|
211
|
+
const logFile = path.join(process.cwd(), "opentunnel.log");
|
|
212
|
+
// Check if already running
|
|
213
|
+
if (fs.existsSync(pidFile)) {
|
|
214
|
+
const oldPid = fs.readFileSync(pidFile, "utf-8").trim();
|
|
215
|
+
try {
|
|
216
|
+
process.kill(parseInt(oldPid), 0);
|
|
217
|
+
console.log(chalk_1.default.yellow(`Server already running (PID: ${oldPid})`));
|
|
218
|
+
console.log(chalk_1.default.gray(`Stop it with: opentunnel stop`));
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
fs.unlinkSync(pidFile);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// Build args without -d flag
|
|
226
|
+
const args = ["server"];
|
|
227
|
+
if (options.port)
|
|
228
|
+
args.push("-p", options.port);
|
|
229
|
+
if (options.publicPort)
|
|
230
|
+
args.push("--public-port", options.publicPort);
|
|
231
|
+
if (options.domain)
|
|
232
|
+
args.push("--domain", options.domain);
|
|
233
|
+
if (options.basePath)
|
|
234
|
+
args.push("-b", options.basePath);
|
|
235
|
+
if (options.host)
|
|
236
|
+
args.push("--host", options.host);
|
|
237
|
+
if (options.tcpMin)
|
|
238
|
+
args.push("--tcp-min", options.tcpMin);
|
|
239
|
+
if (options.tcpMax)
|
|
240
|
+
args.push("--tcp-max", options.tcpMax);
|
|
241
|
+
if (options.authTokens)
|
|
242
|
+
args.push("--auth-tokens", options.authTokens);
|
|
243
|
+
if (options.https)
|
|
244
|
+
args.push("--https");
|
|
245
|
+
if (options.email)
|
|
246
|
+
args.push("--email", options.email);
|
|
247
|
+
if (options.production)
|
|
248
|
+
args.push("--production");
|
|
249
|
+
if (options.cloudflareToken)
|
|
250
|
+
args.push("--cloudflare-token", options.cloudflareToken);
|
|
251
|
+
if (options.duckdnsToken)
|
|
252
|
+
args.push("--duckdns-token", options.duckdnsToken);
|
|
253
|
+
if (options.autoDns)
|
|
254
|
+
args.push("--auto-dns");
|
|
255
|
+
if (options.dnsCreateRecords)
|
|
256
|
+
args.push("--dns-create-records");
|
|
257
|
+
if (options.dnsDeleteOnClose)
|
|
258
|
+
args.push("--dns-delete-on-close");
|
|
259
|
+
const out = fs.openSync(logFile, "a");
|
|
260
|
+
const err = fs.openSync(logFile, "a");
|
|
261
|
+
const child = spawn(process.execPath, [process.argv[1], ...args], {
|
|
262
|
+
detached: true,
|
|
263
|
+
stdio: ["ignore", out, err],
|
|
264
|
+
cwd: process.cwd(),
|
|
265
|
+
});
|
|
266
|
+
child.unref();
|
|
267
|
+
fs.writeFileSync(pidFile, String(child.pid));
|
|
268
|
+
console.log(chalk_1.default.green(`OpenTunnel server started in background`));
|
|
269
|
+
console.log(chalk_1.default.gray(` PID: ${child.pid}`));
|
|
270
|
+
console.log(chalk_1.default.gray(` Port: ${options.port}`));
|
|
271
|
+
console.log(chalk_1.default.gray(` Domain: ${options.domain}`));
|
|
272
|
+
console.log(chalk_1.default.gray(` Log: ${logFile}`));
|
|
273
|
+
console.log(chalk_1.default.gray(` PID file: ${pidFile}`));
|
|
274
|
+
console.log("");
|
|
275
|
+
console.log(chalk_1.default.gray(`Stop with: node dist/cli/index.js stop`));
|
|
276
|
+
console.log(chalk_1.default.gray(`Logs: tail -f ${logFile}`));
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
// Normal foreground mode
|
|
280
|
+
const { TunnelServer } = await Promise.resolve().then(() => __importStar(require("../server/TunnelServer")));
|
|
281
|
+
// Determine HTTPS configuration (self-signed enabled by default)
|
|
282
|
+
let httpsConfig = undefined;
|
|
283
|
+
let selfSignedHttpsConfig = undefined;
|
|
284
|
+
let autoHttpsConfig = undefined;
|
|
285
|
+
if (options.httpsCert && options.httpsKey) {
|
|
286
|
+
// Custom certificates provided
|
|
287
|
+
const fs = await Promise.resolve().then(() => __importStar(require("fs")));
|
|
288
|
+
httpsConfig = {
|
|
289
|
+
cert: fs.readFileSync(options.httpsCert, "utf-8"),
|
|
290
|
+
key: fs.readFileSync(options.httpsKey, "utf-8"),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
else if (options.letsencrypt) {
|
|
294
|
+
// Let's Encrypt
|
|
295
|
+
autoHttpsConfig = {
|
|
296
|
+
enabled: true,
|
|
297
|
+
email: options.email || `admin@${options.domain}`,
|
|
298
|
+
production: options.production || false,
|
|
299
|
+
cloudflareToken: options.cloudflareToken,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
// Self-signed by default (use --no-https to disable)
|
|
304
|
+
selfSignedHttpsConfig = {
|
|
305
|
+
enabled: options.https !== false,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
const server = new TunnelServer({
|
|
309
|
+
port: parseInt(options.port),
|
|
310
|
+
publicPort: options.publicPort ? parseInt(options.publicPort) : undefined,
|
|
311
|
+
host: options.host,
|
|
312
|
+
domain: options.domain,
|
|
313
|
+
basePath: options.basePath,
|
|
314
|
+
tunnelPortRange: {
|
|
315
|
+
min: parseInt(options.tcpMin),
|
|
316
|
+
max: parseInt(options.tcpMax),
|
|
317
|
+
},
|
|
318
|
+
auth: options.authTokens
|
|
319
|
+
? { required: true, tokens: options.authTokens.split(",") }
|
|
320
|
+
: undefined,
|
|
321
|
+
https: httpsConfig,
|
|
322
|
+
selfSignedHttps: selfSignedHttpsConfig,
|
|
323
|
+
autoHttps: autoHttpsConfig,
|
|
324
|
+
autoDns: detectDnsConfig(options),
|
|
325
|
+
});
|
|
326
|
+
// Helper function to auto-detect DNS provider
|
|
327
|
+
function detectDnsConfig(opts) {
|
|
328
|
+
// Auto-detect provider based on tokens or domain
|
|
329
|
+
const domain = opts.domain || "localhost";
|
|
330
|
+
const isDuckDnsDomain = domain.endsWith(".duckdns.org");
|
|
331
|
+
// Priority: explicit token > domain detection
|
|
332
|
+
if (opts.cloudflareToken) {
|
|
333
|
+
return {
|
|
334
|
+
enabled: true,
|
|
335
|
+
provider: "cloudflare",
|
|
336
|
+
cloudflareToken: opts.cloudflareToken,
|
|
337
|
+
createRecords: opts.dnsCreateRecords !== false,
|
|
338
|
+
deleteOnClose: opts.dnsDeleteOnClose || false,
|
|
339
|
+
setupWildcard: true,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
if (opts.duckdnsToken || isDuckDnsDomain) {
|
|
343
|
+
return {
|
|
344
|
+
enabled: true,
|
|
345
|
+
provider: "duckdns",
|
|
346
|
+
duckdnsToken: opts.duckdnsToken,
|
|
347
|
+
createRecords: false, // DuckDNS doesn't support subdomains
|
|
348
|
+
deleteOnClose: false,
|
|
349
|
+
setupWildcard: false,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
// No auto DNS if no tokens provided
|
|
353
|
+
if (opts.autoDns) {
|
|
354
|
+
console.log(chalk_1.default.yellow("Warning: --auto-dns requires --cloudflare-token or --duckdns-token"));
|
|
355
|
+
}
|
|
356
|
+
return undefined;
|
|
357
|
+
}
|
|
358
|
+
console.log(chalk_1.default.cyan(`
|
|
359
|
+
██████╗ ██████╗ ███████╗███╗ ██╗████████╗██╗ ██╗███╗ ██╗███╗ ██╗███████╗██╗
|
|
360
|
+
██╔═══██╗██╔══██╗██╔════╝████╗ ██║╚══██╔══╝██║ ██║████╗ ██║████╗ ██║██╔════╝██║
|
|
361
|
+
██║ ██║██████╔╝█████╗ ██╔██╗ ██║ ██║ ██║ ██║██╔██╗ ██║██╔██╗ ██║█████╗ ██║
|
|
362
|
+
██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║ ██║ ██║ ██║██║╚██╗██║██║╚██╗██║██╔══╝ ██║
|
|
363
|
+
╚██████╔╝██║ ███████╗██║ ╚████║ ██║ ╚██████╔╝██║ ╚████║██║ ╚████║███████╗███████╗
|
|
364
|
+
╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═══╝╚══════╝╚══════╝
|
|
365
|
+
`));
|
|
366
|
+
server.on("tunnel:created", ({ tunnelId, publicUrl }) => {
|
|
367
|
+
console.log(chalk_1.default.green(`[+] Tunnel created: ${publicUrl}`));
|
|
368
|
+
});
|
|
369
|
+
server.on("tunnel:closed", ({ tunnelId }) => {
|
|
370
|
+
console.log(chalk_1.default.yellow(`[-] Tunnel closed: ${tunnelId}`));
|
|
371
|
+
});
|
|
372
|
+
await server.start();
|
|
373
|
+
console.log(chalk_1.default.green(`\nServer running on ${options.host}:${options.port}`));
|
|
374
|
+
console.log(chalk_1.default.gray(`Domain: ${options.domain}`));
|
|
375
|
+
console.log(chalk_1.default.gray(`Subdomain pattern: *.${options.basePath}.${options.domain}`));
|
|
376
|
+
console.log(chalk_1.default.gray(`TCP port range: ${options.tcpMin}-${options.tcpMax}\n`));
|
|
377
|
+
});
|
|
378
|
+
// Stop command
|
|
379
|
+
program
|
|
380
|
+
.command("stop")
|
|
381
|
+
.description("Stop the OpenTunnel server running in background")
|
|
382
|
+
.action(async () => {
|
|
383
|
+
const fs = await Promise.resolve().then(() => __importStar(require("fs")));
|
|
384
|
+
const path = await Promise.resolve().then(() => __importStar(require("path")));
|
|
385
|
+
const pidFile = path.join(process.cwd(), ".opentunnel.pid");
|
|
386
|
+
if (!fs.existsSync(pidFile)) {
|
|
387
|
+
console.log(chalk_1.default.yellow("No server running (PID file not found)"));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const pid = parseInt(fs.readFileSync(pidFile, "utf-8").trim());
|
|
391
|
+
try {
|
|
392
|
+
process.kill(pid, "SIGTERM");
|
|
393
|
+
fs.unlinkSync(pidFile);
|
|
394
|
+
console.log(chalk_1.default.green(`Server stopped (PID: ${pid})`));
|
|
395
|
+
}
|
|
396
|
+
catch (err) {
|
|
397
|
+
if (err.code === "ESRCH") {
|
|
398
|
+
fs.unlinkSync(pidFile);
|
|
399
|
+
console.log(chalk_1.default.yellow(`Server was not running (stale PID file removed)`));
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
console.log(chalk_1.default.red(`Failed to stop server: ${err.message}`));
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
// Logs command
|
|
407
|
+
program
|
|
408
|
+
.command("logs")
|
|
409
|
+
.description("Show server logs")
|
|
410
|
+
.option("-f, --follow", "Follow log output")
|
|
411
|
+
.option("-n, --lines <n>", "Number of lines to show", "50")
|
|
412
|
+
.action(async (options) => {
|
|
413
|
+
const fs = await Promise.resolve().then(() => __importStar(require("fs")));
|
|
414
|
+
const path = await Promise.resolve().then(() => __importStar(require("path")));
|
|
415
|
+
const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
|
|
416
|
+
const logFile = path.join(process.cwd(), "opentunnel.log");
|
|
417
|
+
if (!fs.existsSync(logFile)) {
|
|
418
|
+
console.log(chalk_1.default.yellow("No log file found"));
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (options.follow) {
|
|
422
|
+
// Use tail -f on Unix or PowerShell on Windows
|
|
423
|
+
const isWindows = process.platform === "win32";
|
|
424
|
+
if (isWindows) {
|
|
425
|
+
const child = spawn("powershell", ["-Command", `Get-Content -Path "${logFile}" -Tail ${options.lines} -Wait`], {
|
|
426
|
+
stdio: "inherit"
|
|
427
|
+
});
|
|
428
|
+
child.on("error", () => {
|
|
429
|
+
// Fallback: just read the file
|
|
430
|
+
console.log(fs.readFileSync(logFile, "utf-8"));
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
spawn("tail", ["-f", "-n", options.lines, logFile], { stdio: "inherit" });
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
const content = fs.readFileSync(logFile, "utf-8");
|
|
439
|
+
const lines = content.split("\n");
|
|
440
|
+
const lastLines = lines.slice(-parseInt(options.lines));
|
|
441
|
+
console.log(lastLines.join("\n"));
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
// Status command
|
|
445
|
+
program
|
|
446
|
+
.command("status")
|
|
447
|
+
.description("Check server status")
|
|
448
|
+
.option("-s, --server <url>", "Server URL", "http://localhost:8080")
|
|
449
|
+
.action(async (options) => {
|
|
450
|
+
const spinner = (0, ora_1.default)("Checking server status...").start();
|
|
451
|
+
try {
|
|
452
|
+
const http = await Promise.resolve().then(() => __importStar(require("http")));
|
|
453
|
+
const url = new URL("/api/stats", options.server);
|
|
454
|
+
const response = await new Promise((resolve, reject) => {
|
|
455
|
+
http.get(url.toString(), (res) => {
|
|
456
|
+
let data = "";
|
|
457
|
+
res.on("data", chunk => data += chunk);
|
|
458
|
+
res.on("end", () => resolve(JSON.parse(data)));
|
|
459
|
+
}).on("error", reject);
|
|
460
|
+
});
|
|
461
|
+
spinner.succeed("Server is running");
|
|
462
|
+
console.log(chalk_1.default.gray(` Clients: ${response.clients}`));
|
|
463
|
+
console.log(chalk_1.default.gray(` Tunnels: ${response.tunnels}`));
|
|
464
|
+
console.log(chalk_1.default.gray(` Uptime: ${(0, utils_1.formatDuration)(response.uptime * 1000)}`));
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
spinner.fail("Server is not reachable");
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
// List tunnels command
|
|
471
|
+
program
|
|
472
|
+
.command("list")
|
|
473
|
+
.description("List active tunnels")
|
|
474
|
+
.option("-s, --server <url>", "Server URL", "http://localhost:8080")
|
|
475
|
+
.action(async (options) => {
|
|
476
|
+
const spinner = (0, ora_1.default)("Fetching tunnels...").start();
|
|
477
|
+
try {
|
|
478
|
+
const http = await Promise.resolve().then(() => __importStar(require("http")));
|
|
479
|
+
const url = new URL("/api/tunnels", options.server);
|
|
480
|
+
const response = await new Promise((resolve, reject) => {
|
|
481
|
+
http.get(url.toString(), (res) => {
|
|
482
|
+
let data = "";
|
|
483
|
+
res.on("data", chunk => data += chunk);
|
|
484
|
+
res.on("end", () => resolve(JSON.parse(data)));
|
|
485
|
+
}).on("error", reject);
|
|
486
|
+
});
|
|
487
|
+
spinner.stop();
|
|
488
|
+
if (response.tunnels.length === 0) {
|
|
489
|
+
console.log(chalk_1.default.yellow("No active tunnels"));
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
console.log(chalk_1.default.cyan("\nActive Tunnels:"));
|
|
493
|
+
console.log(chalk_1.default.gray("─".repeat(80)));
|
|
494
|
+
for (const tunnel of response.tunnels) {
|
|
495
|
+
console.log(` ${chalk_1.default.white(tunnel.id)}`);
|
|
496
|
+
console.log(` Protocol: ${chalk_1.default.yellow(tunnel.protocol.toUpperCase())}`);
|
|
497
|
+
console.log(` Local: ${chalk_1.default.gray(tunnel.localAddress)}`);
|
|
498
|
+
console.log(` Public: ${chalk_1.default.green(tunnel.publicUrl)}`);
|
|
499
|
+
console.log(` Traffic: ${chalk_1.default.blue(`↓${(0, utils_1.formatBytes)(tunnel.bytesIn)} ↑${(0, utils_1.formatBytes)(tunnel.bytesOut)}`)}`);
|
|
500
|
+
console.log(chalk_1.default.gray("─".repeat(80)));
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
catch (err) {
|
|
504
|
+
spinner.fail("Failed to fetch tunnels");
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
// Setup/help command - explains requirements
|
|
508
|
+
program
|
|
509
|
+
.command("setup")
|
|
510
|
+
.description("Show setup instructions for running OpenTunnel on a custom domain")
|
|
511
|
+
.option("--domain <domain>", "Show setup for specific domain")
|
|
512
|
+
.action(async (options) => {
|
|
513
|
+
const domain = options.domain || "yourdomain.com";
|
|
514
|
+
console.log(chalk_1.default.cyan(`
|
|
515
|
+
╔══════════════════════════════════════════════════════════════════════════════╗
|
|
516
|
+
║ OpenTunnel Setup Guide ║
|
|
517
|
+
╚══════════════════════════════════════════════════════════════════════════════╝
|
|
518
|
+
`));
|
|
519
|
+
console.log(chalk_1.default.white.bold("1. SERVER REQUIREMENTS"));
|
|
520
|
+
console.log(chalk_1.default.gray("─".repeat(78)));
|
|
521
|
+
console.log(`
|
|
522
|
+
You need a server (VPS, cloud instance, etc.) with:
|
|
523
|
+
${chalk_1.default.green("✓")} Public IP address
|
|
524
|
+
${chalk_1.default.green("✓")} Ports 80 and 443 open (for HTTP/HTTPS)
|
|
525
|
+
${chalk_1.default.green("✓")} Port 8080 open (for WebSocket tunnel, or use reverse proxy)
|
|
526
|
+
${chalk_1.default.green("✓")} Node.js 18+ installed
|
|
527
|
+
`);
|
|
528
|
+
// Try to get public IP for the docs
|
|
529
|
+
let serverIP = "<YOUR_SERVER_IP>";
|
|
530
|
+
try {
|
|
531
|
+
const https = await Promise.resolve().then(() => __importStar(require("https")));
|
|
532
|
+
serverIP = await new Promise((resolve) => {
|
|
533
|
+
https.get("https://api.ipify.org", (res) => {
|
|
534
|
+
let data = "";
|
|
535
|
+
res.on("data", (chunk) => (data += chunk));
|
|
536
|
+
res.on("end", () => resolve(data.trim()));
|
|
537
|
+
}).on("error", () => resolve("<YOUR_SERVER_IP>"));
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
catch { }
|
|
541
|
+
console.log(chalk_1.default.white.bold("2. DNS CONFIGURATION"));
|
|
542
|
+
console.log(chalk_1.default.gray("─".repeat(78)));
|
|
543
|
+
console.log(`
|
|
544
|
+
${chalk_1.default.yellow.bold("Your server's public IP:")} ${chalk_1.default.green(serverIP)}
|
|
545
|
+
|
|
546
|
+
${chalk_1.default.white.bold("Required DNS Records (create in Cloudflare/your DNS provider):")}
|
|
547
|
+
|
|
548
|
+
┌──────────┬─────────────────────────┬─────────────────────┬──────────────┐
|
|
549
|
+
│ ${chalk_1.default.cyan("Type")} │ ${chalk_1.default.cyan("Name")} │ ${chalk_1.default.cyan("Content")} │ ${chalk_1.default.cyan("Proxy")} │
|
|
550
|
+
├──────────┼─────────────────────────┼─────────────────────┼──────────────┤
|
|
551
|
+
│ ${chalk_1.default.yellow("A")} │ op.${domain.padEnd(20)} │ ${serverIP.padEnd(19)} │ ${chalk_1.default.red("OFF (DNS only)")} │
|
|
552
|
+
│ ${chalk_1.default.yellow("A")} │ *.op.${domain.padEnd(18)} │ ${serverIP.padEnd(19)} │ ${chalk_1.default.red("OFF (DNS only)")} │
|
|
553
|
+
└──────────┴─────────────────────────┴─────────────────────┴──────────────┘
|
|
554
|
+
|
|
555
|
+
${chalk_1.default.yellow("⚠ IMPORTANT: Disable Cloudflare Proxy (gray cloud, not orange)")}
|
|
556
|
+
${chalk_1.default.gray(" - Proxy OFF = DNS only (gray cloud) ← Use this")}
|
|
557
|
+
${chalk_1.default.gray(" - Proxy ON = Proxied (orange cloud) ← Don't use")}
|
|
558
|
+
|
|
559
|
+
${chalk_1.default.gray("Why? WebSocket tunnels and TCP don't work well through Cloudflare's proxy.")}
|
|
560
|
+
|
|
561
|
+
${chalk_1.default.gray("─".repeat(40))}
|
|
562
|
+
|
|
563
|
+
${chalk_1.default.green("Option A: Automatic DNS with Cloudflare (Recommended)")}
|
|
564
|
+
${chalk_1.default.gray("If you provide a Cloudflare token, OpenTunnel will create these records automatically!")}
|
|
565
|
+
|
|
566
|
+
${chalk_1.default.cyan("Option B: Manual DNS setup")}
|
|
567
|
+
${chalk_1.default.gray("Create the records above manually in your DNS provider.")}
|
|
568
|
+
|
|
569
|
+
${chalk_1.default.gray("After DNS propagation, these URLs will work:")}
|
|
570
|
+
${chalk_1.default.green(` https://op.${domain}`)} ${chalk_1.default.gray("← Server dashboard")}
|
|
571
|
+
${chalk_1.default.green(` https://myapp.op.${domain}`)} ${chalk_1.default.gray("← Your tunnel")}
|
|
572
|
+
${chalk_1.default.green(` https://api.op.${domain}`)} ${chalk_1.default.gray("← Another tunnel")}
|
|
573
|
+
${chalk_1.default.green(` https://anything.op.${domain}`)} ${chalk_1.default.gray("← Any subdomain")}
|
|
574
|
+
|
|
575
|
+
${chalk_1.default.yellow("Tip:")} You can change 'op' to any prefix you prefer (e.g., 'tunnel', 't', etc.)
|
|
576
|
+
`);
|
|
577
|
+
console.log(chalk_1.default.white.bold("3. SERVER SETUP (Automatic HTTPS + DNS)"));
|
|
578
|
+
console.log(chalk_1.default.gray("─".repeat(78)));
|
|
579
|
+
console.log(`
|
|
580
|
+
On your server, run:
|
|
581
|
+
|
|
582
|
+
${chalk_1.default.cyan("# Clone and install")}
|
|
583
|
+
git clone https://github.com/your-repo/opentunnel.git
|
|
584
|
+
cd opentunnel
|
|
585
|
+
npm install && npm run build
|
|
586
|
+
|
|
587
|
+
${chalk_1.default.cyan("# Option A: Full automatic setup with Cloudflare (Recommended)")}
|
|
588
|
+
${chalk_1.default.gray("Get your Cloudflare API token from: https://dash.cloudflare.com/profile/api-tokens")}
|
|
589
|
+
${chalk_1.default.gray("Required permissions: Zone:DNS:Edit")}
|
|
590
|
+
|
|
591
|
+
${chalk_1.default.green(`sudo node dist/cli/index.js server \\
|
|
592
|
+
--domain ${domain} \\
|
|
593
|
+
--https \\
|
|
594
|
+
--email admin@${domain} \\
|
|
595
|
+
--cloudflare-token YOUR_CF_API_TOKEN \\
|
|
596
|
+
--production -d`)}
|
|
597
|
+
|
|
598
|
+
${chalk_1.default.gray("This will automatically:")}
|
|
599
|
+
${chalk_1.default.green("✓")} Create DNS records (*.op.${domain} and op.${domain})
|
|
600
|
+
${chalk_1.default.green("✓")} Obtain wildcard SSL certificate
|
|
601
|
+
${chalk_1.default.green("✓")} Use DNS-01 challenge (no port 80 needed during setup)
|
|
602
|
+
${chalk_1.default.green("✓")} Listen on port 443 (HTTPS)
|
|
603
|
+
${chalk_1.default.green("✓")} Redirect HTTP (80) to HTTPS
|
|
604
|
+
${chalk_1.default.green("✓")} Auto-renew certificates
|
|
605
|
+
${chalk_1.default.green("✓")} Create individual DNS records per tunnel
|
|
606
|
+
|
|
607
|
+
${chalk_1.default.cyan("# Option B: DNS only (no HTTPS)")}
|
|
608
|
+
${chalk_1.default.green(`node dist/cli/index.js server --domain ${domain} --cloudflare-token YOUR_CF_TOKEN -d`)}
|
|
609
|
+
${chalk_1.default.gray("Creates DNS records automatically, HTTP only (port 8080)")}
|
|
610
|
+
|
|
611
|
+
${chalk_1.default.cyan("# Option C: Manual DNS + HTTPS")}
|
|
612
|
+
${chalk_1.default.yellow("Note: Requires manual DNS wildcard record setup")}
|
|
613
|
+
${chalk_1.default.green(`sudo node dist/cli/index.js server --domain ${domain} --https --email admin@${domain} --production -d`)}
|
|
614
|
+
|
|
615
|
+
${chalk_1.default.cyan("# Option D: Testing/local (no HTTPS, no auto DNS)")}
|
|
616
|
+
node dist/cli/index.js server --domain ${domain} -d
|
|
617
|
+
|
|
618
|
+
${chalk_1.default.cyan("# With authentication")}
|
|
619
|
+
sudo node dist/cli/index.js server --domain ${domain} --https --cloudflare-token CF_TOKEN --auth-tokens "secret" -d
|
|
620
|
+
|
|
621
|
+
${chalk_1.default.yellow("Note: Use 'sudo' for ports 80/443. Or run without --https on port 8080.")}
|
|
622
|
+
`);
|
|
623
|
+
console.log(chalk_1.default.white.bold("4. REVERSE PROXY (Optional - only if needed)"));
|
|
624
|
+
console.log(chalk_1.default.gray("─".repeat(78)));
|
|
625
|
+
console.log(`
|
|
626
|
+
${chalk_1.default.green("OpenTunnel handles HTTPS automatically!")}
|
|
627
|
+
${chalk_1.default.gray("Only use a reverse proxy if you need additional features.")}
|
|
628
|
+
|
|
629
|
+
${chalk_1.default.cyan("# If you prefer using Caddy:")}
|
|
630
|
+
${domain}, *.op.${domain} {
|
|
631
|
+
reverse_proxy localhost:8080
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
${chalk_1.default.cyan("# If you prefer Nginx (requires manual cert setup):")}
|
|
635
|
+
server {
|
|
636
|
+
listen 443 ssl;
|
|
637
|
+
server_name ${domain} *.op.${domain};
|
|
638
|
+
ssl_certificate /etc/letsencrypt/live/${domain}/fullchain.pem;
|
|
639
|
+
ssl_certificate_key /etc/letsencrypt/live/${domain}/privkey.pem;
|
|
640
|
+
location / {
|
|
641
|
+
proxy_pass http://localhost:8080;
|
|
642
|
+
proxy_http_version 1.1;
|
|
643
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
644
|
+
proxy_set_header Connection "upgrade";
|
|
645
|
+
proxy_set_header Host $host;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
`);
|
|
649
|
+
console.log(chalk_1.default.white.bold("5. CLIENT USAGE"));
|
|
650
|
+
console.log(chalk_1.default.gray("─".repeat(78)));
|
|
651
|
+
console.log(`
|
|
652
|
+
From your local machine:
|
|
653
|
+
|
|
654
|
+
${chalk_1.default.cyan("# Connect to your server")}
|
|
655
|
+
opentunnel http 3000 --subdomain myapp --domain ${domain}
|
|
656
|
+
|
|
657
|
+
${chalk_1.default.cyan("# Your local port 3000 will be available at:")}
|
|
658
|
+
${chalk_1.default.green(`https://myapp.op.${domain}`)}
|
|
659
|
+
|
|
660
|
+
${chalk_1.default.cyan("# With authentication token")}
|
|
661
|
+
opentunnel http 3000 -n myapp --domain ${domain} -t "secret-token"
|
|
662
|
+
`);
|
|
663
|
+
console.log(chalk_1.default.white.bold("6. PORT FORWARDING (If behind NAT/Router)"));
|
|
664
|
+
console.log(chalk_1.default.gray("─".repeat(78)));
|
|
665
|
+
console.log(`
|
|
666
|
+
If your server is behind a router:
|
|
667
|
+
|
|
668
|
+
${chalk_1.default.yellow("Router Settings → Port Forwarding:")}
|
|
669
|
+
┌────────────────┬───────────────┬────────────────┐
|
|
670
|
+
│ External Port │ Internal Port │ Protocol │
|
|
671
|
+
├────────────────┼───────────────┼────────────────┤
|
|
672
|
+
│ 80 │ 80 │ TCP │
|
|
673
|
+
│ 443 │ 443 │ TCP │
|
|
674
|
+
│ 8080 │ 8080 │ TCP │
|
|
675
|
+
│ 10000-20000 │ 10000-20000 │ TCP (for TCP) │
|
|
676
|
+
└────────────────┴───────────────┴────────────────┘
|
|
677
|
+
`);
|
|
678
|
+
console.log(chalk_1.default.white.bold("7. VERIFY SETUP"));
|
|
679
|
+
console.log(chalk_1.default.gray("─".repeat(78)));
|
|
680
|
+
console.log(`
|
|
681
|
+
${chalk_1.default.cyan("# Check DNS propagation")}
|
|
682
|
+
nslookup ${domain}
|
|
683
|
+
nslookup test.op.${domain}
|
|
684
|
+
|
|
685
|
+
${chalk_1.default.cyan("# Test server connection")}
|
|
686
|
+
curl -I https://${domain}
|
|
687
|
+
|
|
688
|
+
${chalk_1.default.cyan("# Test WebSocket endpoint")}
|
|
689
|
+
curl -I https://${domain}/_tunnel
|
|
690
|
+
`);
|
|
691
|
+
console.log(chalk_1.default.gray("─".repeat(78)));
|
|
692
|
+
console.log(chalk_1.default.green("\nNeed help? https://github.com/your-repo/opentunnel/issues\n"));
|
|
693
|
+
});
|
|
694
|
+
// Init command - create example config
|
|
695
|
+
program
|
|
696
|
+
.command("init")
|
|
697
|
+
.description("Create an example opentunnel.yml configuration file")
|
|
698
|
+
.option("-f, --force", "Overwrite existing config file")
|
|
699
|
+
.action(async (options) => {
|
|
700
|
+
const fs = await Promise.resolve().then(() => __importStar(require("fs")));
|
|
701
|
+
const path = await Promise.resolve().then(() => __importStar(require("path")));
|
|
702
|
+
const configPath = path.join(process.cwd(), CONFIG_FILE);
|
|
703
|
+
if (fs.existsSync(configPath) && !options.force) {
|
|
704
|
+
console.log(chalk_1.default.yellow(`Config file already exists: ${configPath}`));
|
|
705
|
+
console.log(chalk_1.default.gray("Use --force to overwrite"));
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
const exampleConfig = {
|
|
709
|
+
version: "1.0",
|
|
710
|
+
server: {
|
|
711
|
+
url: "ws://localhost:8080/_tunnel",
|
|
712
|
+
// token: "your-auth-token",
|
|
713
|
+
},
|
|
714
|
+
tunnels: [
|
|
715
|
+
{
|
|
716
|
+
name: "web",
|
|
717
|
+
protocol: "http",
|
|
718
|
+
port: 3000,
|
|
719
|
+
subdomain: "web",
|
|
720
|
+
autostart: true,
|
|
721
|
+
},
|
|
722
|
+
{
|
|
723
|
+
name: "api",
|
|
724
|
+
protocol: "http",
|
|
725
|
+
port: 4000,
|
|
726
|
+
subdomain: "api",
|
|
727
|
+
autostart: true,
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
name: "database",
|
|
731
|
+
protocol: "tcp",
|
|
732
|
+
port: 5432,
|
|
733
|
+
autostart: false,
|
|
734
|
+
},
|
|
735
|
+
],
|
|
736
|
+
};
|
|
737
|
+
fs.writeFileSync(configPath, (0, yaml_1.stringify)(exampleConfig, { indent: 2 }));
|
|
738
|
+
console.log(chalk_1.default.green(`Created ${CONFIG_FILE}`));
|
|
739
|
+
console.log(chalk_1.default.gray(`\nEdit the file to configure your tunnels, then run:`));
|
|
740
|
+
console.log(chalk_1.default.cyan(` opentunnel up # Start all tunnels`));
|
|
741
|
+
console.log(chalk_1.default.cyan(` opentunnel up -d # Start in background`));
|
|
742
|
+
});
|
|
743
|
+
// Up command - start tunnels from config (like docker-compose up)
|
|
744
|
+
program
|
|
745
|
+
.command("up")
|
|
746
|
+
.description("Start server and tunnels from opentunnel.yml (like docker-compose up)")
|
|
747
|
+
.option("-d, --detach", "Run in background (detached mode)")
|
|
748
|
+
.option("-f, --file <path>", "Config file path", CONFIG_FILE)
|
|
749
|
+
.option("--no-autostart", "Ignore autostart setting, start all tunnels")
|
|
750
|
+
.action(async (options) => {
|
|
751
|
+
const fs = await Promise.resolve().then(() => __importStar(require("fs")));
|
|
752
|
+
const path = await Promise.resolve().then(() => __importStar(require("path")));
|
|
753
|
+
const { TunnelServer } = await Promise.resolve().then(() => __importStar(require("../server/TunnelServer")));
|
|
754
|
+
// Load config file
|
|
755
|
+
const configPath = path.join(process.cwd(), options.file);
|
|
756
|
+
let config = { version: "1.0", tunnels: [] };
|
|
757
|
+
if (fs.existsSync(configPath)) {
|
|
758
|
+
const configContent = fs.readFileSync(configPath, "utf-8");
|
|
759
|
+
config = (0, yaml_1.parse)(configContent);
|
|
760
|
+
}
|
|
761
|
+
else {
|
|
762
|
+
console.log(chalk_1.default.red(`Config file not found: ${configPath}`));
|
|
763
|
+
console.log(chalk_1.default.gray(`Run 'opentunnel init' to create one`));
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
const tunnelsToStart = options.autostart === false
|
|
767
|
+
? config.tunnels
|
|
768
|
+
: config.tunnels?.filter(t => t.autostart !== false) || [];
|
|
769
|
+
// Get server config from yml
|
|
770
|
+
const port = config.server?.port || 443;
|
|
771
|
+
const domain = config.server?.domain || "localhost";
|
|
772
|
+
const basePath = config.server?.basePath || "op";
|
|
773
|
+
const useHttps = config.server?.https !== false;
|
|
774
|
+
const tcpMin = config.server?.tcpPortMin || 10000;
|
|
775
|
+
const tcpMax = config.server?.tcpPortMax || 20000;
|
|
776
|
+
// Display banner
|
|
777
|
+
console.log(chalk_1.default.cyan(`
|
|
778
|
+
██████╗ ██████╗ ███████╗███╗ ██╗████████╗██╗ ██╗███╗ ██╗███╗ ██╗███████╗██╗
|
|
779
|
+
██╔═══██╗██╔══██╗██╔════╝████╗ ██║╚══██╔══╝██║ ██║████╗ ██║████╗ ██║██╔════╝██║
|
|
780
|
+
██║ ██║██████╔╝█████╗ ██╔██╗ ██║ ██║ ██║ ██║██╔██╗ ██║██╔██╗ ██║█████╗ ██║
|
|
781
|
+
██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║ ██║ ██║ ██║██║╚██╗██║██║╚██╗██║██╔══╝ ██║
|
|
782
|
+
╚██████╔╝██║ ███████╗██║ ╚████║ ██║ ╚██████╔╝██║ ╚████║██║ ╚████║███████╗███████╗
|
|
783
|
+
╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═══╝╚══════╝╚══════╝
|
|
784
|
+
`));
|
|
785
|
+
// Start server
|
|
786
|
+
const spinner = (0, ora_1.default)("Starting server...").start();
|
|
787
|
+
const server = new TunnelServer({
|
|
788
|
+
port,
|
|
789
|
+
host: "0.0.0.0",
|
|
790
|
+
domain,
|
|
791
|
+
basePath,
|
|
792
|
+
tunnelPortRange: {
|
|
793
|
+
min: tcpMin,
|
|
794
|
+
max: tcpMax,
|
|
795
|
+
},
|
|
796
|
+
selfSignedHttps: useHttps ? { enabled: true } : undefined,
|
|
797
|
+
});
|
|
798
|
+
try {
|
|
799
|
+
await server.start();
|
|
800
|
+
spinner.succeed(`Server running on https://${basePath}.${domain}:${port}`);
|
|
801
|
+
// Start tunnels if defined
|
|
802
|
+
if (tunnelsToStart.length > 0) {
|
|
803
|
+
console.log(chalk_1.default.cyan(`\nStarting ${tunnelsToStart.length} tunnel(s)...\n`));
|
|
804
|
+
const protocol = useHttps ? "wss" : "ws";
|
|
805
|
+
const serverUrl = `${protocol}://localhost:${port}/_tunnel`;
|
|
806
|
+
// Small delay to ensure server is fully ready
|
|
807
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
808
|
+
await startTunnelsFromConfig(tunnelsToStart, serverUrl, config.server?.token, true);
|
|
809
|
+
}
|
|
810
|
+
else {
|
|
811
|
+
console.log(chalk_1.default.yellow("\nNo tunnels defined in config"));
|
|
812
|
+
}
|
|
813
|
+
console.log(chalk_1.default.gray("\nPress Ctrl+C to stop"));
|
|
814
|
+
// Keep running
|
|
815
|
+
await new Promise(() => { });
|
|
816
|
+
}
|
|
817
|
+
catch (error) {
|
|
818
|
+
spinner.fail(`Failed to start: ${error.message}`);
|
|
819
|
+
process.exit(1);
|
|
820
|
+
}
|
|
821
|
+
});
|
|
822
|
+
// Down command - stop all tunnels (like docker-compose down)
|
|
823
|
+
program
|
|
824
|
+
.command("down")
|
|
825
|
+
.description("Stop all running tunnels (like docker-compose down)")
|
|
826
|
+
.action(async () => {
|
|
827
|
+
const fs = await Promise.resolve().then(() => __importStar(require("fs")));
|
|
828
|
+
const path = await Promise.resolve().then(() => __importStar(require("path")));
|
|
829
|
+
// Find all tunnel PID files
|
|
830
|
+
const pidFiles = fs.readdirSync(process.cwd())
|
|
831
|
+
.filter(f => f.startsWith(".opentunnel-") && f.endsWith(".pid"));
|
|
832
|
+
// Also include the server PID
|
|
833
|
+
const serverPidFile = ".opentunnel.pid";
|
|
834
|
+
if (fs.existsSync(path.join(process.cwd(), serverPidFile))) {
|
|
835
|
+
pidFiles.push(serverPidFile);
|
|
836
|
+
}
|
|
837
|
+
if (pidFiles.length === 0) {
|
|
838
|
+
console.log(chalk_1.default.yellow("No tunnels running"));
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
console.log(chalk_1.default.cyan(`Stopping ${pidFiles.length} process(es)...\n`));
|
|
842
|
+
for (const pidFile of pidFiles) {
|
|
843
|
+
const pidPath = path.join(process.cwd(), pidFile);
|
|
844
|
+
const pid = parseInt(fs.readFileSync(pidPath, "utf-8").trim());
|
|
845
|
+
const name = pidFile.replace(".opentunnel-", "").replace(".pid", "").replace(".opentunnel", "server");
|
|
846
|
+
try {
|
|
847
|
+
process.kill(pid, "SIGTERM");
|
|
848
|
+
fs.unlinkSync(pidPath);
|
|
849
|
+
console.log(chalk_1.default.green(` ✓ Stopped ${name} (PID: ${pid})`));
|
|
850
|
+
}
|
|
851
|
+
catch (err) {
|
|
852
|
+
if (err.code === "ESRCH") {
|
|
853
|
+
fs.unlinkSync(pidPath);
|
|
854
|
+
console.log(chalk_1.default.yellow(` - ${name} was not running (cleaned up)`));
|
|
855
|
+
}
|
|
856
|
+
else {
|
|
857
|
+
console.log(chalk_1.default.red(` ✗ Failed to stop ${name}: ${err.message}`));
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
// Clean up log files
|
|
862
|
+
const logFiles = fs.readdirSync(process.cwd())
|
|
863
|
+
.filter(f => f.startsWith("opentunnel") && f.endsWith(".log"));
|
|
864
|
+
if (logFiles.length > 0) {
|
|
865
|
+
console.log(chalk_1.default.gray(`\nLog files preserved: ${logFiles.join(", ")}`));
|
|
866
|
+
}
|
|
867
|
+
console.log(chalk_1.default.green("\nAll tunnels stopped"));
|
|
868
|
+
});
|
|
869
|
+
// PS command - list running tunnel processes (like docker ps)
|
|
870
|
+
program
|
|
871
|
+
.command("ps")
|
|
872
|
+
.description("List running tunnel processes (like docker ps)")
|
|
873
|
+
.action(async () => {
|
|
874
|
+
const fs = await Promise.resolve().then(() => __importStar(require("fs")));
|
|
875
|
+
const path = await Promise.resolve().then(() => __importStar(require("path")));
|
|
876
|
+
const pidFiles = fs.readdirSync(process.cwd())
|
|
877
|
+
.filter(f => f.startsWith(".opentunnel") && f.endsWith(".pid"));
|
|
878
|
+
if (pidFiles.length === 0) {
|
|
879
|
+
console.log(chalk_1.default.yellow("No tunnels running"));
|
|
880
|
+
console.log(chalk_1.default.gray("Start tunnels with: opentunnel up -d"));
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
console.log(chalk_1.default.cyan("\nRunning Processes:"));
|
|
884
|
+
console.log(chalk_1.default.gray("─".repeat(60)));
|
|
885
|
+
console.log(chalk_1.default.gray(` ${"NAME".padEnd(20)} ${"PID".padEnd(10)} ${"STATUS".padEnd(10)}`));
|
|
886
|
+
console.log(chalk_1.default.gray("─".repeat(60)));
|
|
887
|
+
for (const pidFile of pidFiles) {
|
|
888
|
+
const pidPath = path.join(process.cwd(), pidFile);
|
|
889
|
+
const pid = parseInt(fs.readFileSync(pidPath, "utf-8").trim());
|
|
890
|
+
let name = pidFile.replace(".opentunnel-", "").replace(".pid", "").replace(".opentunnel", "");
|
|
891
|
+
if (name === "")
|
|
892
|
+
name = "server";
|
|
893
|
+
let status = "unknown";
|
|
894
|
+
let statusColor = chalk_1.default.gray;
|
|
895
|
+
try {
|
|
896
|
+
process.kill(pid, 0); // Check if process exists
|
|
897
|
+
status = "running";
|
|
898
|
+
statusColor = chalk_1.default.green;
|
|
899
|
+
}
|
|
900
|
+
catch {
|
|
901
|
+
status = "stopped";
|
|
902
|
+
statusColor = chalk_1.default.red;
|
|
903
|
+
}
|
|
904
|
+
console.log(` ${chalk_1.default.white(name.padEnd(20))} ${chalk_1.default.gray(String(pid).padEnd(10))} ${statusColor(status)}`);
|
|
905
|
+
}
|
|
906
|
+
console.log(chalk_1.default.gray("─".repeat(60)));
|
|
907
|
+
console.log(chalk_1.default.gray(`\nStop all: opentunnel down`));
|
|
908
|
+
});
|
|
909
|
+
// Test server command - simple HTTP server for testing tunnels
|
|
910
|
+
program
|
|
911
|
+
.command("test-server")
|
|
912
|
+
.description("Start a simple HTTP test server")
|
|
913
|
+
.option("-p, --port <port>", "Port to listen on", "3000")
|
|
914
|
+
.option("-d, --detach", "Run in background")
|
|
915
|
+
.action(async (options) => {
|
|
916
|
+
const http = await Promise.resolve().then(() => __importStar(require("http")));
|
|
917
|
+
const port = parseInt(options.port);
|
|
918
|
+
if (options.detach) {
|
|
919
|
+
const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
|
|
920
|
+
const fs = await Promise.resolve().then(() => __importStar(require("fs")));
|
|
921
|
+
const path = await Promise.resolve().then(() => __importStar(require("path")));
|
|
922
|
+
const pidFile = path.join(process.cwd(), `.test-server-${port}.pid`);
|
|
923
|
+
const logFile = path.join(process.cwd(), `test-server-${port}.log`);
|
|
924
|
+
if (fs.existsSync(pidFile)) {
|
|
925
|
+
const oldPid = fs.readFileSync(pidFile, "utf-8").trim();
|
|
926
|
+
try {
|
|
927
|
+
process.kill(parseInt(oldPid), 0);
|
|
928
|
+
console.log(chalk_1.default.yellow(`Test server already running on port ${port} (PID: ${oldPid})`));
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
catch {
|
|
932
|
+
fs.unlinkSync(pidFile);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
const out = fs.openSync(logFile, "a");
|
|
936
|
+
const err = fs.openSync(logFile, "a");
|
|
937
|
+
const child = spawn(process.execPath, [process.argv[1], "test-server", "-p", String(port)], {
|
|
938
|
+
detached: true,
|
|
939
|
+
stdio: ["ignore", out, err],
|
|
940
|
+
cwd: process.cwd(),
|
|
941
|
+
});
|
|
942
|
+
child.unref();
|
|
943
|
+
fs.writeFileSync(pidFile, String(child.pid));
|
|
944
|
+
console.log(chalk_1.default.green(`Test server started on port ${port}`));
|
|
945
|
+
console.log(chalk_1.default.gray(` PID: ${child.pid}`));
|
|
946
|
+
console.log(chalk_1.default.gray(` URL: http://localhost:${port}`));
|
|
947
|
+
console.log(chalk_1.default.gray(` Log: ${logFile}`));
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
const server = http.createServer((req, res) => {
|
|
951
|
+
const timestamp = new Date().toISOString();
|
|
952
|
+
const method = req.method;
|
|
953
|
+
const url = req.url;
|
|
954
|
+
const headers = JSON.stringify(req.headers, null, 2);
|
|
955
|
+
console.log(chalk_1.default.cyan(`[${timestamp}] ${method} ${url}`));
|
|
956
|
+
// Collect body for POST/PUT
|
|
957
|
+
let body = "";
|
|
958
|
+
req.on("data", (chunk) => { body += chunk; });
|
|
959
|
+
req.on("end", () => {
|
|
960
|
+
const response = {
|
|
961
|
+
success: true,
|
|
962
|
+
message: "OpenTunnel Test Server",
|
|
963
|
+
request: {
|
|
964
|
+
method,
|
|
965
|
+
url,
|
|
966
|
+
headers: req.headers,
|
|
967
|
+
body: body || undefined,
|
|
968
|
+
},
|
|
969
|
+
server: {
|
|
970
|
+
port,
|
|
971
|
+
timestamp,
|
|
972
|
+
uptime: process.uptime(),
|
|
973
|
+
},
|
|
974
|
+
};
|
|
975
|
+
res.writeHead(200, {
|
|
976
|
+
"Content-Type": "application/json",
|
|
977
|
+
"X-Powered-By": "OpenTunnel Test Server",
|
|
978
|
+
});
|
|
979
|
+
res.end(JSON.stringify(response, null, 2));
|
|
980
|
+
});
|
|
981
|
+
});
|
|
982
|
+
server.listen(port, () => {
|
|
983
|
+
console.log(chalk_1.default.green(`\n OpenTunnel Test Server`));
|
|
984
|
+
console.log(chalk_1.default.gray(" ─────────────────────────────────────────"));
|
|
985
|
+
console.log(` ${chalk_1.default.white("Status:")} ${chalk_1.default.green("● Running")}`);
|
|
986
|
+
console.log(` ${chalk_1.default.white("Port:")} ${chalk_1.default.cyan(port)}`);
|
|
987
|
+
console.log(` ${chalk_1.default.white("URL:")} ${chalk_1.default.cyan(`http://localhost:${port}`)}`);
|
|
988
|
+
console.log(chalk_1.default.gray(" ─────────────────────────────────────────"));
|
|
989
|
+
console.log(chalk_1.default.gray("\n Endpoints:"));
|
|
990
|
+
console.log(chalk_1.default.gray(` GET / → Returns server info`));
|
|
991
|
+
console.log(chalk_1.default.gray(` GET /health → Health check`));
|
|
992
|
+
console.log(chalk_1.default.gray(` POST /echo → Echo request body`));
|
|
993
|
+
console.log(chalk_1.default.gray(` ANY /* → Returns request details`));
|
|
994
|
+
console.log(chalk_1.default.gray("\n Press Ctrl+C to stop\n"));
|
|
995
|
+
});
|
|
996
|
+
process.on("SIGINT", () => {
|
|
997
|
+
console.log(chalk_1.default.yellow("\n Shutting down..."));
|
|
998
|
+
server.close(() => {
|
|
999
|
+
console.log(chalk_1.default.green(" Test server stopped"));
|
|
1000
|
+
process.exit(0);
|
|
1001
|
+
});
|
|
1002
|
+
});
|
|
1003
|
+
});
|
|
1004
|
+
// Stop test servers command
|
|
1005
|
+
program
|
|
1006
|
+
.command("test-server-stop")
|
|
1007
|
+
.description("Stop all test servers")
|
|
1008
|
+
.option("-p, --port <port>", "Stop specific port")
|
|
1009
|
+
.action(async (options) => {
|
|
1010
|
+
const fs = await Promise.resolve().then(() => __importStar(require("fs")));
|
|
1011
|
+
const path = await Promise.resolve().then(() => __importStar(require("path")));
|
|
1012
|
+
let pidFiles;
|
|
1013
|
+
if (options.port) {
|
|
1014
|
+
const specific = `.test-server-${options.port}.pid`;
|
|
1015
|
+
pidFiles = fs.existsSync(path.join(process.cwd(), specific)) ? [specific] : [];
|
|
1016
|
+
}
|
|
1017
|
+
else {
|
|
1018
|
+
pidFiles = fs.readdirSync(process.cwd())
|
|
1019
|
+
.filter(f => f.startsWith(".test-server-") && f.endsWith(".pid"));
|
|
1020
|
+
}
|
|
1021
|
+
if (pidFiles.length === 0) {
|
|
1022
|
+
console.log(chalk_1.default.yellow("No test servers running"));
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
for (const pidFile of pidFiles) {
|
|
1026
|
+
const pidPath = path.join(process.cwd(), pidFile);
|
|
1027
|
+
const pid = parseInt(fs.readFileSync(pidPath, "utf-8").trim());
|
|
1028
|
+
const port = pidFile.replace(".test-server-", "").replace(".pid", "");
|
|
1029
|
+
try {
|
|
1030
|
+
process.kill(pid, "SIGTERM");
|
|
1031
|
+
fs.unlinkSync(pidPath);
|
|
1032
|
+
console.log(chalk_1.default.green(` ✓ Stopped test server on port ${port} (PID: ${pid})`));
|
|
1033
|
+
}
|
|
1034
|
+
catch (err) {
|
|
1035
|
+
if (err.code === "ESRCH") {
|
|
1036
|
+
fs.unlinkSync(pidPath);
|
|
1037
|
+
console.log(chalk_1.default.yellow(` - Test server on port ${port} was not running`));
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
});
|
|
1042
|
+
async function runTunnelInBackgroundFromConfig(name, protocol, port, options) {
|
|
1043
|
+
const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
|
|
1044
|
+
const fs = await Promise.resolve().then(() => __importStar(require("fs")));
|
|
1045
|
+
const path = await Promise.resolve().then(() => __importStar(require("path")));
|
|
1046
|
+
const pidFile = path.join(process.cwd(), `.opentunnel-${name}.pid`);
|
|
1047
|
+
const logFile = path.join(process.cwd(), `opentunnel-${name}.log`);
|
|
1048
|
+
// Check if already running
|
|
1049
|
+
if (fs.existsSync(pidFile)) {
|
|
1050
|
+
const oldPid = fs.readFileSync(pidFile, "utf-8").trim();
|
|
1051
|
+
try {
|
|
1052
|
+
process.kill(parseInt(oldPid), 0);
|
|
1053
|
+
console.log(chalk_1.default.yellow(` - ${name}: already running (PID: ${oldPid})`));
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
catch {
|
|
1057
|
+
fs.unlinkSync(pidFile);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
// Build args
|
|
1061
|
+
const args = [protocol, String(port)];
|
|
1062
|
+
if (options.subdomain)
|
|
1063
|
+
args.push("-n", options.subdomain);
|
|
1064
|
+
if (options.server)
|
|
1065
|
+
args.push("-s", options.server);
|
|
1066
|
+
if (options.token)
|
|
1067
|
+
args.push("-t", options.token);
|
|
1068
|
+
if (options.host)
|
|
1069
|
+
args.push("-h", options.host);
|
|
1070
|
+
if (options.remotePort)
|
|
1071
|
+
args.push("-r", String(options.remotePort));
|
|
1072
|
+
const out = fs.openSync(logFile, "a");
|
|
1073
|
+
const err = fs.openSync(logFile, "a");
|
|
1074
|
+
const child = spawn(process.execPath, [process.argv[1], ...args], {
|
|
1075
|
+
detached: true,
|
|
1076
|
+
stdio: ["ignore", out, err],
|
|
1077
|
+
cwd: process.cwd(),
|
|
1078
|
+
});
|
|
1079
|
+
child.unref();
|
|
1080
|
+
fs.writeFileSync(pidFile, String(child.pid));
|
|
1081
|
+
console.log(chalk_1.default.green(` ✓ ${name}: started (PID: ${child.pid})`));
|
|
1082
|
+
}
|
|
1083
|
+
async function startTunnelsFromConfig(tunnels, serverUrl, token, insecure) {
|
|
1084
|
+
const spinner = (0, ora_1.default)("Connecting to server...").start();
|
|
1085
|
+
const client = new TunnelClient_1.TunnelClient({
|
|
1086
|
+
serverUrl,
|
|
1087
|
+
token,
|
|
1088
|
+
reconnect: true,
|
|
1089
|
+
silent: true,
|
|
1090
|
+
rejectUnauthorized: !insecure,
|
|
1091
|
+
});
|
|
1092
|
+
try {
|
|
1093
|
+
await client.connect();
|
|
1094
|
+
spinner.succeed("Connected to server");
|
|
1095
|
+
const activeTunnels = [];
|
|
1096
|
+
for (const tunnel of tunnels) {
|
|
1097
|
+
const tunnelSpinner = (0, ora_1.default)(`Creating tunnel: ${tunnel.name}...`).start();
|
|
1098
|
+
try {
|
|
1099
|
+
const { tunnelId, publicUrl } = await client.createTunnel({
|
|
1100
|
+
protocol: tunnel.protocol,
|
|
1101
|
+
localHost: tunnel.host || "localhost",
|
|
1102
|
+
localPort: tunnel.port,
|
|
1103
|
+
subdomain: tunnel.subdomain,
|
|
1104
|
+
remotePort: tunnel.remotePort,
|
|
1105
|
+
});
|
|
1106
|
+
activeTunnels.push({ name: tunnel.name, tunnelId, publicUrl });
|
|
1107
|
+
tunnelSpinner.succeed(`${tunnel.name}: ${publicUrl}`);
|
|
1108
|
+
}
|
|
1109
|
+
catch (err) {
|
|
1110
|
+
tunnelSpinner.fail(`${tunnel.name}: ${err.message}`);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
if (activeTunnels.length === 0) {
|
|
1114
|
+
console.log(chalk_1.default.red("\nNo tunnels created"));
|
|
1115
|
+
process.exit(1);
|
|
1116
|
+
}
|
|
1117
|
+
console.log(chalk_1.default.cyan("\n─────────────────────────────────────────"));
|
|
1118
|
+
console.log(chalk_1.default.green(` ${activeTunnels.length} tunnel(s) active`));
|
|
1119
|
+
console.log(chalk_1.default.cyan("─────────────────────────────────────────\n"));
|
|
1120
|
+
for (const t of activeTunnels) {
|
|
1121
|
+
console.log(` ${chalk_1.default.white(t.name.padEnd(15))} ${chalk_1.default.green(t.publicUrl)}`);
|
|
1122
|
+
}
|
|
1123
|
+
console.log(chalk_1.default.gray("\n Press Ctrl+C to stop all tunnels\n"));
|
|
1124
|
+
// Keep alive with uptime counter
|
|
1125
|
+
const startTime = Date.now();
|
|
1126
|
+
const statsInterval = setInterval(() => {
|
|
1127
|
+
const uptime = (0, utils_1.formatDuration)(Date.now() - startTime);
|
|
1128
|
+
process.stdout.write(`\r ${chalk_1.default.gray(`Uptime: ${uptime}`)}`);
|
|
1129
|
+
}, 1000);
|
|
1130
|
+
// Handle exit
|
|
1131
|
+
const cleanup = async () => {
|
|
1132
|
+
clearInterval(statsInterval);
|
|
1133
|
+
console.log("\n");
|
|
1134
|
+
const closeSpinner = (0, ora_1.default)("Closing tunnels...").start();
|
|
1135
|
+
for (const t of activeTunnels) {
|
|
1136
|
+
await client.closeTunnel(t.tunnelId);
|
|
1137
|
+
}
|
|
1138
|
+
await client.disconnect();
|
|
1139
|
+
closeSpinner.succeed("All tunnels closed");
|
|
1140
|
+
process.exit(0);
|
|
1141
|
+
};
|
|
1142
|
+
process.on("SIGINT", cleanup);
|
|
1143
|
+
process.on("SIGTERM", cleanup);
|
|
1144
|
+
// Handle reconnection
|
|
1145
|
+
client.on("disconnected", () => {
|
|
1146
|
+
console.log(chalk_1.default.yellow("\n Disconnected, reconnecting..."));
|
|
1147
|
+
});
|
|
1148
|
+
client.on("connected", () => {
|
|
1149
|
+
console.log(chalk_1.default.green(" Reconnected!"));
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
catch (err) {
|
|
1153
|
+
spinner.fail(`Failed: ${err.message}`);
|
|
1154
|
+
process.exit(1);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
async function runTunnelInBackground(command, port, options) {
|
|
1158
|
+
const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
|
|
1159
|
+
const fs = await Promise.resolve().then(() => __importStar(require("fs")));
|
|
1160
|
+
const path = await Promise.resolve().then(() => __importStar(require("path")));
|
|
1161
|
+
const tunnelId = `tunnel-${port}-${Date.now()}`;
|
|
1162
|
+
const pidFile = path.join(process.cwd(), `.opentunnel-${port}.pid`);
|
|
1163
|
+
const logFile = path.join(process.cwd(), `opentunnel-${port}.log`);
|
|
1164
|
+
// Check if already running
|
|
1165
|
+
if (fs.existsSync(pidFile)) {
|
|
1166
|
+
const oldPid = fs.readFileSync(pidFile, "utf-8").trim();
|
|
1167
|
+
try {
|
|
1168
|
+
process.kill(parseInt(oldPid), 0);
|
|
1169
|
+
console.log(chalk_1.default.yellow(`Tunnel already running on port ${port} (PID: ${oldPid})`));
|
|
1170
|
+
console.log(chalk_1.default.gray(`Stop it with: kill ${oldPid}`));
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
catch {
|
|
1174
|
+
fs.unlinkSync(pidFile);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
// Build args without -d flag
|
|
1178
|
+
const args = [command, port];
|
|
1179
|
+
if (options.subdomain)
|
|
1180
|
+
args.push("-n", options.subdomain);
|
|
1181
|
+
if (options.server)
|
|
1182
|
+
args.push("-s", options.server);
|
|
1183
|
+
if (options.token)
|
|
1184
|
+
args.push("-t", options.token);
|
|
1185
|
+
if (options.host)
|
|
1186
|
+
args.push("-h", options.host);
|
|
1187
|
+
if (options.https)
|
|
1188
|
+
args.push("--https");
|
|
1189
|
+
if (options.ngrok)
|
|
1190
|
+
args.push("--ngrok");
|
|
1191
|
+
if (options.region)
|
|
1192
|
+
args.push("--region", options.region);
|
|
1193
|
+
if (options.remotePort)
|
|
1194
|
+
args.push("-r", options.remotePort);
|
|
1195
|
+
if (options.protocol)
|
|
1196
|
+
args.push("-p", options.protocol);
|
|
1197
|
+
const out = fs.openSync(logFile, "a");
|
|
1198
|
+
const err = fs.openSync(logFile, "a");
|
|
1199
|
+
const child = spawn(process.execPath, [process.argv[1], ...args], {
|
|
1200
|
+
detached: true,
|
|
1201
|
+
stdio: ["ignore", out, err],
|
|
1202
|
+
cwd: process.cwd(),
|
|
1203
|
+
});
|
|
1204
|
+
child.unref();
|
|
1205
|
+
fs.writeFileSync(pidFile, String(child.pid));
|
|
1206
|
+
// Extract domain from server URL for display
|
|
1207
|
+
const serverUrl = options.server || "ws://localhost:8080/_tunnel";
|
|
1208
|
+
let displayDomain = "localhost:8080";
|
|
1209
|
+
let expectedUrl = `http://${options.subdomain || "random"}.op.localhost:8080`;
|
|
1210
|
+
try {
|
|
1211
|
+
const url = new URL(serverUrl.replace("wss://", "https://").replace("ws://", "http://"));
|
|
1212
|
+
displayDomain = url.host;
|
|
1213
|
+
const isSecure = serverUrl.startsWith("wss://");
|
|
1214
|
+
const protocol = isSecure ? "https" : "http";
|
|
1215
|
+
const subdomain = options.subdomain || "<random>";
|
|
1216
|
+
expectedUrl = `${protocol}://${subdomain}.op.${url.hostname}`;
|
|
1217
|
+
if ((isSecure && url.port && url.port !== "443") || (!isSecure && url.port && url.port !== "80")) {
|
|
1218
|
+
expectedUrl += `:${url.port}`;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
catch { }
|
|
1222
|
+
console.log(chalk_1.default.green(`Tunnel started in background`));
|
|
1223
|
+
console.log(chalk_1.default.gray(" ─────────────────────────────────────────"));
|
|
1224
|
+
console.log(` ${chalk_1.default.white("PID:")} ${chalk_1.default.cyan(child.pid)}`);
|
|
1225
|
+
console.log(` ${chalk_1.default.white("Local:")} ${chalk_1.default.gray(`localhost:${port}`)}`);
|
|
1226
|
+
console.log(` ${chalk_1.default.white("Server:")} ${chalk_1.default.gray(displayDomain)}`);
|
|
1227
|
+
console.log(` ${chalk_1.default.white("Public:")} ${chalk_1.default.green(expectedUrl)} ${chalk_1.default.yellow("(pending)")}`);
|
|
1228
|
+
console.log(chalk_1.default.gray(" ─────────────────────────────────────────"));
|
|
1229
|
+
console.log(chalk_1.default.gray(` Log: ${logFile}`));
|
|
1230
|
+
console.log("");
|
|
1231
|
+
console.log(chalk_1.default.gray(`Stop with: kill ${child.pid}`));
|
|
1232
|
+
console.log(chalk_1.default.gray(`Check: tail -f ${logFile}`));
|
|
1233
|
+
}
|
|
1234
|
+
async function createTunnel(options) {
|
|
1235
|
+
const spinner = (0, ora_1.default)("Connecting to server...").start();
|
|
1236
|
+
const client = new TunnelClient_1.TunnelClient({
|
|
1237
|
+
serverUrl: options.serverUrl,
|
|
1238
|
+
token: options.token,
|
|
1239
|
+
reconnect: true,
|
|
1240
|
+
silent: true,
|
|
1241
|
+
rejectUnauthorized: !options.insecure,
|
|
1242
|
+
});
|
|
1243
|
+
try {
|
|
1244
|
+
await client.connect();
|
|
1245
|
+
spinner.text = "Creating tunnel...";
|
|
1246
|
+
const { tunnelId, publicUrl } = await client.createTunnel({
|
|
1247
|
+
protocol: options.protocol,
|
|
1248
|
+
localHost: options.localHost,
|
|
1249
|
+
localPort: options.localPort,
|
|
1250
|
+
subdomain: options.subdomain,
|
|
1251
|
+
remotePort: options.remotePort,
|
|
1252
|
+
});
|
|
1253
|
+
spinner.succeed("Tunnel established!");
|
|
1254
|
+
printTunnelInfo({
|
|
1255
|
+
status: "Online",
|
|
1256
|
+
protocol: options.protocol,
|
|
1257
|
+
localHost: options.localHost,
|
|
1258
|
+
localPort: options.localPort,
|
|
1259
|
+
publicUrl,
|
|
1260
|
+
provider: "OpenTunnel",
|
|
1261
|
+
});
|
|
1262
|
+
// Keep alive
|
|
1263
|
+
const startTime = Date.now();
|
|
1264
|
+
const statsInterval = setInterval(() => {
|
|
1265
|
+
const uptime = (0, utils_1.formatDuration)(Date.now() - startTime);
|
|
1266
|
+
process.stdout.write(`\r ${chalk_1.default.gray(`Uptime: ${uptime}`)}`);
|
|
1267
|
+
}, 1000);
|
|
1268
|
+
// Handle exit
|
|
1269
|
+
const cleanup = async () => {
|
|
1270
|
+
clearInterval(statsInterval);
|
|
1271
|
+
console.log("\n");
|
|
1272
|
+
spinner.start("Closing tunnel...");
|
|
1273
|
+
await client.closeTunnel(tunnelId);
|
|
1274
|
+
await client.disconnect();
|
|
1275
|
+
spinner.succeed("Tunnel closed");
|
|
1276
|
+
process.exit(0);
|
|
1277
|
+
};
|
|
1278
|
+
process.on("SIGINT", cleanup);
|
|
1279
|
+
process.on("SIGTERM", cleanup);
|
|
1280
|
+
// Handle reconnection
|
|
1281
|
+
client.on("disconnected", () => {
|
|
1282
|
+
console.log(chalk_1.default.yellow("\n Disconnected, reconnecting..."));
|
|
1283
|
+
});
|
|
1284
|
+
client.on("connected", () => {
|
|
1285
|
+
console.log(chalk_1.default.green(" Reconnected!"));
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
catch (err) {
|
|
1289
|
+
spinner.fail(`Failed: ${err.message}`);
|
|
1290
|
+
process.exit(1);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
async function createNgrokTunnel(options) {
|
|
1294
|
+
const spinner = (0, ora_1.default)("Starting ngrok...").start();
|
|
1295
|
+
const client = new NgrokClient_1.NgrokClient({
|
|
1296
|
+
authtoken: options.authtoken,
|
|
1297
|
+
region: options.region,
|
|
1298
|
+
});
|
|
1299
|
+
try {
|
|
1300
|
+
await client.connect();
|
|
1301
|
+
spinner.text = "Creating tunnel...";
|
|
1302
|
+
const { tunnelId, publicUrl } = await client.createTunnel({
|
|
1303
|
+
protocol: options.protocol,
|
|
1304
|
+
localHost: options.localHost,
|
|
1305
|
+
localPort: options.localPort,
|
|
1306
|
+
subdomain: options.subdomain,
|
|
1307
|
+
remotePort: options.remotePort,
|
|
1308
|
+
});
|
|
1309
|
+
spinner.succeed("Tunnel established!");
|
|
1310
|
+
printTunnelInfo({
|
|
1311
|
+
status: "Online",
|
|
1312
|
+
protocol: options.protocol,
|
|
1313
|
+
localHost: options.localHost,
|
|
1314
|
+
localPort: options.localPort,
|
|
1315
|
+
publicUrl,
|
|
1316
|
+
provider: "ngrok",
|
|
1317
|
+
});
|
|
1318
|
+
// Keep alive
|
|
1319
|
+
const startTime = Date.now();
|
|
1320
|
+
const statsInterval = setInterval(() => {
|
|
1321
|
+
const uptime = (0, utils_1.formatDuration)(Date.now() - startTime);
|
|
1322
|
+
process.stdout.write(`\r ${chalk_1.default.gray(`Uptime: ${uptime}`)}`);
|
|
1323
|
+
}, 1000);
|
|
1324
|
+
// Handle exit
|
|
1325
|
+
const cleanup = async () => {
|
|
1326
|
+
clearInterval(statsInterval);
|
|
1327
|
+
console.log("\n");
|
|
1328
|
+
spinner.start("Closing tunnel...");
|
|
1329
|
+
await client.closeTunnel(tunnelId);
|
|
1330
|
+
await client.disconnect();
|
|
1331
|
+
spinner.succeed("Tunnel closed");
|
|
1332
|
+
process.exit(0);
|
|
1333
|
+
};
|
|
1334
|
+
process.on("SIGINT", cleanup);
|
|
1335
|
+
process.on("SIGTERM", cleanup);
|
|
1336
|
+
}
|
|
1337
|
+
catch (err) {
|
|
1338
|
+
spinner.fail(`Failed: ${err.message}`);
|
|
1339
|
+
console.log(chalk_1.default.yellow("\nMake sure ngrok is installed: https://ngrok.com/download"));
|
|
1340
|
+
process.exit(1);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
function printTunnelInfo(info) {
|
|
1344
|
+
console.log("");
|
|
1345
|
+
console.log(chalk_1.default.cyan(` OpenTunnel ${chalk_1.default.gray(`(via ${info.provider})`)}`));
|
|
1346
|
+
console.log(chalk_1.default.gray(" ─────────────────────────────────────────"));
|
|
1347
|
+
console.log(` ${chalk_1.default.white("Status:")} ${chalk_1.default.green(`● ${info.status}`)}`);
|
|
1348
|
+
console.log(` ${chalk_1.default.white("Protocol:")} ${chalk_1.default.yellow(info.protocol.toUpperCase())}`);
|
|
1349
|
+
console.log(` ${chalk_1.default.white("Local:")} ${chalk_1.default.gray(`${info.localHost}:${info.localPort}`)}`);
|
|
1350
|
+
console.log(` ${chalk_1.default.white("Public:")} ${chalk_1.default.green(info.publicUrl)}`);
|
|
1351
|
+
console.log(chalk_1.default.gray(" ─────────────────────────────────────────"));
|
|
1352
|
+
console.log("");
|
|
1353
|
+
console.log(chalk_1.default.gray(" Press Ctrl+C to close the tunnel"));
|
|
1354
|
+
console.log("");
|
|
1355
|
+
}
|
|
1356
|
+
program.parse();
|
|
1357
|
+
//# sourceMappingURL=index.js.map
|