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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +284 -0
  3. package/dist/cli/index.d.ts +3 -0
  4. package/dist/cli/index.d.ts.map +1 -0
  5. package/dist/cli/index.js +1357 -0
  6. package/dist/cli/index.js.map +1 -0
  7. package/dist/client/NgrokClient.d.ts +40 -0
  8. package/dist/client/NgrokClient.d.ts.map +1 -0
  9. package/dist/client/NgrokClient.js +155 -0
  10. package/dist/client/NgrokClient.js.map +1 -0
  11. package/dist/client/TunnelClient.d.ts +47 -0
  12. package/dist/client/TunnelClient.d.ts.map +1 -0
  13. package/dist/client/TunnelClient.js +435 -0
  14. package/dist/client/TunnelClient.js.map +1 -0
  15. package/dist/client/index.d.ts +3 -0
  16. package/dist/client/index.d.ts.map +1 -0
  17. package/dist/client/index.js +8 -0
  18. package/dist/client/index.js.map +1 -0
  19. package/dist/dns/CloudflareDNS.d.ts +45 -0
  20. package/dist/dns/CloudflareDNS.d.ts.map +1 -0
  21. package/dist/dns/CloudflareDNS.js +286 -0
  22. package/dist/dns/CloudflareDNS.js.map +1 -0
  23. package/dist/dns/DuckDNS.d.ts +20 -0
  24. package/dist/dns/DuckDNS.d.ts.map +1 -0
  25. package/dist/dns/DuckDNS.js +109 -0
  26. package/dist/dns/DuckDNS.js.map +1 -0
  27. package/dist/dns/index.d.ts +3 -0
  28. package/dist/dns/index.d.ts.map +1 -0
  29. package/dist/dns/index.js +9 -0
  30. package/dist/dns/index.js.map +1 -0
  31. package/dist/index.d.ts +7 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +35 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/server/CertManager.d.ts +54 -0
  36. package/dist/server/CertManager.d.ts.map +1 -0
  37. package/dist/server/CertManager.js +414 -0
  38. package/dist/server/CertManager.js.map +1 -0
  39. package/dist/server/TunnelServer.d.ts +42 -0
  40. package/dist/server/TunnelServer.d.ts.map +1 -0
  41. package/dist/server/TunnelServer.js +790 -0
  42. package/dist/server/TunnelServer.js.map +1 -0
  43. package/dist/server/index.d.ts +3 -0
  44. package/dist/server/index.d.ts.map +1 -0
  45. package/dist/server/index.js +48 -0
  46. package/dist/server/index.js.map +1 -0
  47. package/dist/shared/types.d.ts +147 -0
  48. package/dist/shared/types.d.ts.map +1 -0
  49. package/dist/shared/types.js +3 -0
  50. package/dist/shared/types.js.map +1 -0
  51. package/dist/shared/utils.d.ts +29 -0
  52. package/dist/shared/utils.d.ts.map +1 -0
  53. package/dist/shared/utils.js +135 -0
  54. package/dist/shared/utils.js.map +1 -0
  55. package/package.json +66 -0
@@ -0,0 +1,790 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.TunnelServer = void 0;
7
+ const http_1 = __importDefault(require("http"));
8
+ const https_1 = __importDefault(require("https"));
9
+ const net_1 = __importDefault(require("net"));
10
+ const ws_1 = require("ws");
11
+ const events_1 = require("events");
12
+ const utils_1 = require("../shared/utils");
13
+ const CertManager_1 = require("./CertManager");
14
+ const CloudflareDNS_1 = require("../dns/CloudflareDNS");
15
+ const DuckDNS_1 = require("../dns/DuckDNS");
16
+ class TunnelServer extends events_1.EventEmitter {
17
+ constructor(config) {
18
+ super();
19
+ this.httpRedirectServer = null;
20
+ this.clients = new Map();
21
+ this.tunnelsBySubdomain = new Map();
22
+ this.tunnelsByPort = new Map();
23
+ this.usedPorts = new Set();
24
+ this.pendingRequests = new Map();
25
+ this.keepaliveInterval = null;
26
+ this.certManager = null;
27
+ this.isHttps = false;
28
+ this.dnsProvider = null;
29
+ this.config = {
30
+ port: 8080,
31
+ host: "0.0.0.0",
32
+ domain: "localhost",
33
+ basePath: "op",
34
+ tunnelPortRange: { min: 10000, max: 20000 },
35
+ ...config,
36
+ };
37
+ this.logger = new utils_1.Logger("Server");
38
+ // Create HTTP server initially (will be upgraded to HTTPS if needed)
39
+ this.httpServer = http_1.default.createServer();
40
+ // Setup request handler
41
+ this.httpServer.on("request", this.handleHttpRequest.bind(this));
42
+ // Create WebSocket server
43
+ this.wss = new ws_1.WebSocketServer({ noServer: true });
44
+ // Handle upgrade requests
45
+ this.httpServer.on("upgrade", (request, socket, head) => {
46
+ const url = new URL(request.url || "/", `http://${request.headers.host}`);
47
+ if (url.pathname === "/_tunnel") {
48
+ this.wss.handleUpgrade(request, socket, head, (ws) => {
49
+ this.handleConnection(ws, request);
50
+ });
51
+ }
52
+ else {
53
+ socket.destroy();
54
+ }
55
+ });
56
+ }
57
+ async setupHttps() {
58
+ if (this.config.https) {
59
+ // Manual HTTPS with provided certificates
60
+ this.logger.info("Setting up HTTPS with provided certificates");
61
+ this.httpServer = https_1.default.createServer({
62
+ cert: this.config.https.cert,
63
+ key: this.config.https.key,
64
+ });
65
+ this.isHttps = true;
66
+ }
67
+ else if (this.config.selfSignedHttps?.enabled) {
68
+ // Self-signed certificates (local/development)
69
+ this.logger.info("Setting up HTTPS with self-signed certificate");
70
+ this.certManager = new CertManager_1.CertManager({
71
+ certsDir: this.config.selfSignedHttps.certsDir,
72
+ });
73
+ // Generate self-signed certificate for the domain
74
+ const domain = `${this.config.basePath}.${this.config.domain}`;
75
+ const certInfo = this.certManager.getOrCreateSelfSignedCert(domain);
76
+ this.httpServer = https_1.default.createServer({
77
+ cert: certInfo.cert,
78
+ key: certInfo.key,
79
+ });
80
+ this.isHttps = true;
81
+ this.logger.info(`Self-signed certificate valid until: ${certInfo.expiresAt.toISOString()}`);
82
+ this.logger.warn("⚠️ Using self-signed certificate - browsers will show security warning");
83
+ }
84
+ else if (this.config.autoHttps?.enabled) {
85
+ // Automatic HTTPS with Let's Encrypt
86
+ this.logger.info("Setting up automatic HTTPS with Let's Encrypt");
87
+ this.certManager = new CertManager_1.CertManager({
88
+ certsDir: this.config.autoHttps.certsDir,
89
+ email: this.config.autoHttps.email,
90
+ production: this.config.autoHttps.production,
91
+ cloudflareToken: this.config.autoHttps.cloudflareToken,
92
+ });
93
+ await this.certManager.initialize();
94
+ // Determine domains to get certificate for
95
+ const wildcardDomain = `*.${this.config.basePath}.${this.config.domain}`;
96
+ const hasCloudflare = this.certManager.hasCloudflare();
97
+ // With Cloudflare, we can get wildcard certificates via DNS-01
98
+ // Without Cloudflare, we can only get single-domain certs via HTTP-01
99
+ const domains = hasCloudflare
100
+ ? [this.config.domain, wildcardDomain]
101
+ : [this.config.domain];
102
+ // If using Cloudflare, find the zone first
103
+ if (hasCloudflare) {
104
+ const zoneId = await this.certManager.findCloudflareZone(this.config.domain);
105
+ if (!zoneId) {
106
+ throw new Error(`Could not find Cloudflare zone for domain: ${this.config.domain}`);
107
+ }
108
+ }
109
+ // Check for existing certificate
110
+ let certInfo = await this.certManager.getCertificate(domains);
111
+ if (!certInfo) {
112
+ this.logger.info(`Requesting SSL certificate for: ${domains.join(", ")}`);
113
+ // For HTTP-01 challenges (non-wildcard), start challenge server
114
+ if (!hasCloudflare) {
115
+ const challengeServer = this.certManager.createChallengeServer();
116
+ await new Promise((resolve) => {
117
+ challengeServer.listen(80, "0.0.0.0", () => {
118
+ this.logger.info("ACME challenge server started on port 80");
119
+ resolve();
120
+ });
121
+ });
122
+ try {
123
+ certInfo = await this.certManager.requestCertificate(domains);
124
+ }
125
+ finally {
126
+ challengeServer.close();
127
+ }
128
+ }
129
+ else {
130
+ // DNS-01 challenge via Cloudflare (no HTTP server needed)
131
+ this.logger.info("Using DNS-01 challenge via Cloudflare for wildcard certificate");
132
+ certInfo = await this.certManager.requestCertificate(domains);
133
+ }
134
+ }
135
+ // Create HTTPS server with the certificate
136
+ this.httpServer = https_1.default.createServer({
137
+ cert: certInfo.cert,
138
+ key: certInfo.key,
139
+ });
140
+ // Create HTTP redirect server
141
+ this.httpRedirectServer = http_1.default.createServer((req, res) => {
142
+ const host = req.headers.host || this.config.domain;
143
+ // Handle ACME challenge
144
+ if (req.url?.startsWith("/.well-known/acme-challenge/") && this.certManager) {
145
+ const token = req.url.split("/").pop() || "";
146
+ const keyAuth = this.certManager.handleChallengeRequest(token);
147
+ if (keyAuth) {
148
+ res.writeHead(200, { "Content-Type": "text/plain" });
149
+ res.end(keyAuth);
150
+ return;
151
+ }
152
+ }
153
+ // Redirect to HTTPS
154
+ res.writeHead(301, { Location: `https://${host}${req.url}` });
155
+ res.end();
156
+ });
157
+ this.isHttps = true;
158
+ this.logger.info(`SSL certificate valid until: ${certInfo.expiresAt.toISOString()}`);
159
+ }
160
+ // Re-setup request handlers for new server
161
+ if (this.isHttps) {
162
+ this.httpServer.on("request", this.handleHttpRequest.bind(this));
163
+ this.httpServer.on("upgrade", (request, socket, head) => {
164
+ const url = new URL(request.url || "/", `https://${request.headers.host}`);
165
+ if (url.pathname === "/_tunnel") {
166
+ this.wss.handleUpgrade(request, socket, head, (ws) => {
167
+ this.handleConnection(ws, request);
168
+ });
169
+ }
170
+ else {
171
+ socket.destroy();
172
+ }
173
+ });
174
+ }
175
+ }
176
+ async start() {
177
+ // Setup HTTPS if configured
178
+ await this.setupHttps();
179
+ // Setup automatic DNS if configured (disabled - manual DNS configuration preferred)
180
+ // await this.setupAutoDns();
181
+ return new Promise((resolve) => {
182
+ const port = this.isHttps ? (this.config.port === 8080 ? 443 : this.config.port) : this.config.port;
183
+ this.httpServer.listen(port, this.config.host, () => {
184
+ const protocol = this.isHttps ? "https" : "http";
185
+ this.logger.info(`Server started on ${this.config.host}:${port} (${protocol.toUpperCase()})`);
186
+ this.logger.info(`Domain: ${this.config.domain}`);
187
+ this.logger.info(`Subdomain pattern: *.${this.config.basePath}.${this.config.domain}`);
188
+ if (this.isHttps) {
189
+ this.logger.info(`SSL: Enabled (Let's Encrypt)`);
190
+ }
191
+ if (this.dnsProvider) {
192
+ this.logger.info(`DNS: Automatic management enabled (${this.dnsProvider.name})`);
193
+ }
194
+ // Start HTTP redirect server if using HTTPS
195
+ if (this.httpRedirectServer) {
196
+ this.httpRedirectServer.listen(80, this.config.host, () => {
197
+ this.logger.info("HTTP→HTTPS redirect server on port 80");
198
+ });
199
+ }
200
+ // Start keepalive interval to ping clients and detect dead connections
201
+ this.startKeepalive();
202
+ resolve();
203
+ });
204
+ });
205
+ }
206
+ async setupAutoDns() {
207
+ if (!this.config.autoDns?.enabled) {
208
+ return;
209
+ }
210
+ const provider = this.detectDnsProvider();
211
+ if (provider === "cloudflare" && this.config.autoDns.cloudflareToken) {
212
+ this.logger.info("Auto-detected DNS provider: Cloudflare");
213
+ this.logger.info("Setting up automatic DNS management...");
214
+ const cfProvider = new CloudflareDNS_1.CloudflareDNS(this.config.autoDns.cloudflareToken, this.config.domain, this.config.basePath);
215
+ const initialized = await cfProvider.initialize();
216
+ if (!initialized) {
217
+ this.logger.error("Failed to initialize Cloudflare DNS - automatic DNS disabled");
218
+ return;
219
+ }
220
+ this.dnsProvider = cfProvider;
221
+ // Setup wildcard and base records if requested
222
+ if (this.config.autoDns.setupWildcard !== false) {
223
+ const success = await cfProvider.setupDNS();
224
+ if (success) {
225
+ this.logger.info("DNS records configured successfully");
226
+ }
227
+ else {
228
+ this.logger.warn("Some DNS records could not be configured");
229
+ }
230
+ }
231
+ }
232
+ else if (provider === "duckdns" && this.config.autoDns.duckdnsToken) {
233
+ this.logger.info("Auto-detected DNS provider: DuckDNS");
234
+ const duckProvider = new DuckDNS_1.DuckDNS(this.config.autoDns.duckdnsToken);
235
+ this.dnsProvider = duckProvider;
236
+ // Update DuckDNS with server's public IP
237
+ const publicIP = await duckProvider.getPublicIP();
238
+ const duckdnsDomain = this.config.domain.replace(".duckdns.org", "");
239
+ const success = await duckProvider.updateRecord(duckdnsDomain, publicIP);
240
+ if (success) {
241
+ this.logger.info(`DuckDNS updated: ${this.config.domain} -> ${publicIP}`);
242
+ }
243
+ else {
244
+ this.logger.warn("Failed to update DuckDNS record");
245
+ }
246
+ this.logger.info("Note: DuckDNS uses wildcard DNS - all subdomains will resolve automatically");
247
+ }
248
+ else {
249
+ this.logger.warn("No DNS provider configured or token missing");
250
+ this.logger.info("Provide --cloudflare-token or --duckdns-token for automatic DNS");
251
+ }
252
+ }
253
+ detectDnsProvider() {
254
+ // Explicit provider setting takes priority
255
+ if (this.config.autoDns?.provider) {
256
+ return this.config.autoDns.provider;
257
+ }
258
+ // Auto-detect based on tokens
259
+ if (this.config.autoDns?.cloudflareToken) {
260
+ return "cloudflare";
261
+ }
262
+ if (this.config.autoDns?.duckdnsToken) {
263
+ return "duckdns";
264
+ }
265
+ // Auto-detect based on domain
266
+ if (this.config.domain.endsWith(".duckdns.org")) {
267
+ return "duckdns";
268
+ }
269
+ return null;
270
+ }
271
+ stop() {
272
+ return new Promise((resolve) => {
273
+ // Stop keepalive
274
+ this.stopKeepalive();
275
+ // Close all tunnels
276
+ for (const client of this.clients.values()) {
277
+ for (const tunnel of client.tunnels.values()) {
278
+ this.closeTunnel(tunnel);
279
+ }
280
+ client.ws.close();
281
+ }
282
+ // Close HTTP redirect server if exists
283
+ if (this.httpRedirectServer) {
284
+ this.httpRedirectServer.close();
285
+ }
286
+ this.wss.close(() => {
287
+ this.httpServer.close(() => {
288
+ this.logger.info("Server stopped");
289
+ resolve();
290
+ });
291
+ });
292
+ });
293
+ }
294
+ startKeepalive() {
295
+ // Ping all clients every 20 seconds
296
+ this.keepaliveInterval = setInterval(() => {
297
+ for (const client of this.clients.values()) {
298
+ if (!client.isAlive) {
299
+ // Client didn't respond to last ping, terminate
300
+ this.logger.warn(`Client ${client.id} not responding, terminating`);
301
+ client.ws.terminate();
302
+ continue;
303
+ }
304
+ // Mark as not alive, will be set back to true when pong received
305
+ client.isAlive = false;
306
+ // Send WebSocket-level ping
307
+ if (client.ws.readyState === ws_1.WebSocket.OPEN) {
308
+ client.ws.ping();
309
+ }
310
+ }
311
+ }, 20000);
312
+ }
313
+ stopKeepalive() {
314
+ if (this.keepaliveInterval) {
315
+ clearInterval(this.keepaliveInterval);
316
+ this.keepaliveInterval = null;
317
+ }
318
+ }
319
+ handleConnection(ws, request) {
320
+ const clientId = (0, utils_1.generateId)();
321
+ const client = {
322
+ id: clientId,
323
+ ws,
324
+ authenticated: !this.config.auth?.required,
325
+ tunnels: new Map(),
326
+ createdAt: new Date(),
327
+ lastPong: Date.now(),
328
+ isAlive: true,
329
+ };
330
+ this.clients.set(clientId, client);
331
+ this.logger.info(`Client connected: ${clientId}`);
332
+ // Handle WebSocket-level pong (response to our ping)
333
+ ws.on("pong", () => {
334
+ client.isAlive = true;
335
+ client.lastPong = Date.now();
336
+ });
337
+ ws.on("message", (data) => {
338
+ // Any message counts as activity
339
+ client.isAlive = true;
340
+ client.lastPong = Date.now();
341
+ try {
342
+ const message = (0, utils_1.decodeMessage)(data.toString());
343
+ this.handleMessage(client, message);
344
+ }
345
+ catch (err) {
346
+ this.logger.error(`Failed to parse message from ${clientId}`);
347
+ }
348
+ });
349
+ ws.on("close", () => {
350
+ this.logger.info(`Client disconnected: ${clientId}`);
351
+ // Close all tunnels for this client
352
+ for (const tunnel of client.tunnels.values()) {
353
+ this.closeTunnel(tunnel);
354
+ }
355
+ this.clients.delete(clientId);
356
+ });
357
+ ws.on("error", (err) => {
358
+ this.logger.error(`Client error ${clientId}:`, err.message);
359
+ });
360
+ // Send auth response if no auth required
361
+ if (!this.config.auth?.required) {
362
+ this.send(ws, {
363
+ type: "auth_response",
364
+ id: (0, utils_1.generateId)(),
365
+ timestamp: Date.now(),
366
+ success: true,
367
+ clientId,
368
+ });
369
+ }
370
+ }
371
+ handleMessage(client, message) {
372
+ switch (message.type) {
373
+ case "auth":
374
+ this.handleAuth(client, message);
375
+ break;
376
+ case "tunnel_request":
377
+ this.handleTunnelRequest(client, message);
378
+ break;
379
+ case "tunnel_close":
380
+ this.handleTunnelClose(client, message.id);
381
+ break;
382
+ case "http_response":
383
+ this.handleHttpResponse(message);
384
+ break;
385
+ case "tcp_data":
386
+ this.handleTcpData(message);
387
+ break;
388
+ case "ping":
389
+ this.send(client.ws, { type: "pong", id: message.id, timestamp: Date.now() });
390
+ break;
391
+ }
392
+ }
393
+ handleAuth(client, message) {
394
+ let success = false;
395
+ if (this.config.auth?.required) {
396
+ if (message.token && this.config.auth.tokens.includes(message.token)) {
397
+ success = true;
398
+ client.authenticated = true;
399
+ }
400
+ }
401
+ else {
402
+ success = true;
403
+ client.authenticated = true;
404
+ }
405
+ this.send(client.ws, {
406
+ type: "auth_response",
407
+ id: (0, utils_1.generateId)(),
408
+ timestamp: Date.now(),
409
+ success,
410
+ clientId: success ? client.id : undefined,
411
+ error: success ? undefined : "Invalid token",
412
+ });
413
+ }
414
+ handleTunnelRequest(client, message) {
415
+ if (!client.authenticated) {
416
+ this.send(client.ws, {
417
+ type: "tunnel_response",
418
+ id: (0, utils_1.generateId)(),
419
+ timestamp: Date.now(),
420
+ success: false,
421
+ error: "Not authenticated",
422
+ });
423
+ return;
424
+ }
425
+ const config = message.config;
426
+ const tunnelId = (0, utils_1.generateId)();
427
+ let publicUrl;
428
+ if (config.protocol === "http" || config.protocol === "https") {
429
+ // HTTP tunnel - use subdomain pattern: subdomain.op.domain.com
430
+ const subdomain = config.subdomain || (0, utils_1.generateSubdomain)();
431
+ if (this.tunnelsBySubdomain.has(subdomain)) {
432
+ this.send(client.ws, {
433
+ type: "tunnel_response",
434
+ id: (0, utils_1.generateId)(),
435
+ timestamp: Date.now(),
436
+ success: false,
437
+ error: `Subdomain '${subdomain}' is already in use`,
438
+ });
439
+ return;
440
+ }
441
+ const protocol = this.isHttps ? "https" : "http";
442
+ // Third-level subdomain pattern: myapp.op.domain.com
443
+ publicUrl = `${protocol}://${subdomain}.${this.config.basePath}.${this.config.domain}`;
444
+ // Add port to URL only if not default (80 for http, 443 for https)
445
+ const publicPort = this.config.publicPort || this.config.port;
446
+ const isDefaultPort = (protocol === "http" && publicPort === 80) ||
447
+ (protocol === "https" && publicPort === 443);
448
+ if (!isDefaultPort) {
449
+ publicUrl += `:${publicPort}`;
450
+ }
451
+ const tunnel = {
452
+ id: tunnelId,
453
+ config: { ...config, id: tunnelId, subdomain },
454
+ client,
455
+ publicUrl,
456
+ tcpConnections: new Map(),
457
+ stats: { bytesIn: 0, bytesOut: 0, connections: 0 },
458
+ createdAt: new Date(),
459
+ };
460
+ this.tunnelsBySubdomain.set(subdomain, tunnel);
461
+ client.tunnels.set(tunnelId, tunnel);
462
+ this.logger.info(`HTTP tunnel: ${subdomain}.${this.config.basePath}.${this.config.domain} -> ${config.localHost}:${config.localPort}`);
463
+ // Create DNS record if auto DNS is enabled (Cloudflare only)
464
+ if (this.dnsProvider &&
465
+ this.config.autoDns?.createRecords !== false &&
466
+ this.dnsProvider instanceof CloudflareDNS_1.CloudflareDNS) {
467
+ this.dnsProvider.updateRecord(subdomain).then((success) => {
468
+ if (success) {
469
+ this.logger.info(`DNS record created for ${subdomain}`);
470
+ }
471
+ }).catch((err) => {
472
+ this.logger.warn(`Failed to create DNS record: ${err.message}`);
473
+ });
474
+ }
475
+ }
476
+ else if (config.protocol === "tcp") {
477
+ // TCP tunnel - allocate a port
478
+ // Priority: 1. Explicit remotePort, 2. Same as localPort (if available), 3. Next available
479
+ let port = null;
480
+ if (config.remotePort) {
481
+ // User explicitly requested this port
482
+ if (this.usedPorts.has(config.remotePort)) {
483
+ this.send(client.ws, {
484
+ type: "tunnel_response",
485
+ id: (0, utils_1.generateId)(),
486
+ timestamp: Date.now(),
487
+ success: false,
488
+ error: `Port ${config.remotePort} is already in use`,
489
+ });
490
+ return;
491
+ }
492
+ port = config.remotePort;
493
+ }
494
+ else {
495
+ // Try to use the same port as local (if within range and available)
496
+ const localPort = config.localPort;
497
+ const isInRange = localPort >= this.config.tunnelPortRange.min &&
498
+ localPort <= this.config.tunnelPortRange.max;
499
+ if (isInRange && !this.usedPorts.has(localPort)) {
500
+ port = localPort;
501
+ }
502
+ else {
503
+ // Fallback to next available port
504
+ port = (0, utils_1.getNextAvailablePort)(this.config.tunnelPortRange.min, this.config.tunnelPortRange.max, this.usedPorts);
505
+ }
506
+ }
507
+ if (!port) {
508
+ this.send(client.ws, {
509
+ type: "tunnel_response",
510
+ id: (0, utils_1.generateId)(),
511
+ timestamp: Date.now(),
512
+ success: false,
513
+ error: "No available ports",
514
+ });
515
+ return;
516
+ }
517
+ publicUrl = `tcp://${this.config.domain}:${port}`;
518
+ const tunnel = {
519
+ id: tunnelId,
520
+ config: { ...config, id: tunnelId, remotePort: port },
521
+ client,
522
+ publicUrl,
523
+ tcpConnections: new Map(),
524
+ stats: { bytesIn: 0, bytesOut: 0, connections: 0 },
525
+ createdAt: new Date(),
526
+ };
527
+ // Create TCP server for this tunnel
528
+ const tcpServer = net_1.default.createServer((socket) => {
529
+ this.handleTcpConnection(tunnel, socket);
530
+ });
531
+ tcpServer.listen(port, this.config.host, () => {
532
+ this.logger.info(`TCP tunnel on port ${port} -> ${config.localHost}:${config.localPort}`);
533
+ });
534
+ tunnel.tcpServer = tcpServer;
535
+ this.usedPorts.add(port);
536
+ this.tunnelsByPort.set(port, tunnel);
537
+ client.tunnels.set(tunnelId, tunnel);
538
+ }
539
+ else {
540
+ this.send(client.ws, {
541
+ type: "tunnel_response",
542
+ id: (0, utils_1.generateId)(),
543
+ timestamp: Date.now(),
544
+ success: false,
545
+ error: `Unsupported protocol: ${config.protocol}`,
546
+ });
547
+ return;
548
+ }
549
+ this.send(client.ws, {
550
+ type: "tunnel_response",
551
+ id: (0, utils_1.generateId)(),
552
+ timestamp: Date.now(),
553
+ success: true,
554
+ tunnelId,
555
+ publicUrl,
556
+ });
557
+ this.emit("tunnel:created", { tunnelId, publicUrl, config });
558
+ }
559
+ handleTunnelClose(client, tunnelId) {
560
+ const tunnel = client.tunnels.get(tunnelId);
561
+ if (tunnel) {
562
+ this.closeTunnel(tunnel);
563
+ }
564
+ }
565
+ closeTunnel(tunnel) {
566
+ // Close TCP server if exists
567
+ if (tunnel.tcpServer) {
568
+ tunnel.tcpServer.close();
569
+ const port = tunnel.config.remotePort;
570
+ this.usedPorts.delete(port);
571
+ this.tunnelsByPort.delete(port);
572
+ }
573
+ // Close all TCP connections
574
+ for (const socket of tunnel.tcpConnections.values()) {
575
+ socket.destroy();
576
+ }
577
+ // Remove from subdomain map and delete DNS record
578
+ if (tunnel.config.subdomain) {
579
+ this.tunnelsBySubdomain.delete(tunnel.config.subdomain);
580
+ // Delete DNS record if auto DNS is enabled and deleteOnClose is true (Cloudflare only)
581
+ if (this.dnsProvider &&
582
+ this.config.autoDns?.deleteOnClose &&
583
+ this.dnsProvider instanceof CloudflareDNS_1.CloudflareDNS) {
584
+ this.dnsProvider.deleteRecord(tunnel.config.subdomain).then((success) => {
585
+ if (success) {
586
+ this.logger.info(`DNS record deleted for ${tunnel.config.subdomain}`);
587
+ }
588
+ }).catch((err) => {
589
+ this.logger.warn(`Failed to delete DNS record: ${err.message}`);
590
+ });
591
+ }
592
+ }
593
+ // Remove from client
594
+ tunnel.client.tunnels.delete(tunnel.id);
595
+ this.logger.info(`Tunnel closed: ${tunnel.id}`);
596
+ this.emit("tunnel:closed", { tunnelId: tunnel.id });
597
+ }
598
+ async handleHttpRequest(req, res) {
599
+ const host = req.headers.host || "";
600
+ const hostWithoutPort = host.split(":")[0];
601
+ // Check if this is a direct request to the server (API or status)
602
+ if (hostWithoutPort === this.config.domain ||
603
+ hostWithoutPort === `${this.config.basePath}.${this.config.domain}`) {
604
+ if (req.url?.startsWith("/api/")) {
605
+ this.handleApiRequest(req, res);
606
+ return;
607
+ }
608
+ // Serve status page
609
+ res.writeHead(200, { "Content-Type": "application/json" });
610
+ res.end(JSON.stringify({
611
+ name: "OpenTunnel Server",
612
+ version: "1.0.0",
613
+ status: "running",
614
+ domain: this.config.domain,
615
+ subdomainPattern: `*.${this.config.basePath}.${this.config.domain}`,
616
+ tunnels: this.getTunnelCount(),
617
+ clients: this.clients.size,
618
+ }));
619
+ return;
620
+ }
621
+ // Find tunnel by subdomain (pattern: subdomain.op.domain.com)
622
+ const subdomain = (0, utils_1.extractSubdomain)(hostWithoutPort, this.config.domain, this.config.basePath);
623
+ if (!subdomain) {
624
+ res.writeHead(404, { "Content-Type": "application/json" });
625
+ res.end(JSON.stringify({ error: "Tunnel not found" }));
626
+ return;
627
+ }
628
+ const tunnel = this.tunnelsBySubdomain.get(subdomain);
629
+ if (!tunnel) {
630
+ res.writeHead(404, { "Content-Type": "application/json" });
631
+ res.end(JSON.stringify({ error: `Tunnel '${subdomain}' not found` }));
632
+ return;
633
+ }
634
+ // Forward request to client
635
+ const requestId = (0, utils_1.generateId)();
636
+ const bodyChunks = [];
637
+ req.on("data", (chunk) => bodyChunks.push(chunk));
638
+ req.on("end", async () => {
639
+ const body = Buffer.concat(bodyChunks).toString();
640
+ const httpRequest = {
641
+ type: "http_request",
642
+ id: (0, utils_1.generateId)(),
643
+ timestamp: Date.now(),
644
+ tunnelId: tunnel.id,
645
+ requestId,
646
+ method: req.method || "GET",
647
+ path: req.url || "/",
648
+ headers: req.headers,
649
+ body: body || undefined,
650
+ };
651
+ tunnel.stats.bytesIn += body.length;
652
+ tunnel.stats.connections++;
653
+ try {
654
+ const response = await this.waitForResponse(tunnel, requestId, httpRequest);
655
+ res.writeHead(response.statusCode, response.headers);
656
+ if (response.body) {
657
+ // Decode base64 if the response body is encoded (for binary data like gzip)
658
+ const bodyBuffer = response.isBase64
659
+ ? Buffer.from(response.body, "base64")
660
+ : Buffer.from(response.body, "utf-8");
661
+ tunnel.stats.bytesOut += bodyBuffer.length;
662
+ res.end(bodyBuffer);
663
+ }
664
+ else {
665
+ res.end();
666
+ }
667
+ }
668
+ catch (err) {
669
+ res.writeHead(502, { "Content-Type": "application/json" });
670
+ res.end(JSON.stringify({ error: "Bad gateway - tunnel client not responding" }));
671
+ }
672
+ });
673
+ }
674
+ waitForResponse(tunnel, requestId, request) {
675
+ return new Promise((resolve, reject) => {
676
+ const timeout = setTimeout(() => {
677
+ this.pendingRequests.delete(requestId);
678
+ reject(new Error("Request timeout"));
679
+ }, 30000);
680
+ this.pendingRequests.set(requestId, { resolve, reject, timeout });
681
+ this.send(tunnel.client.ws, request);
682
+ });
683
+ }
684
+ handleHttpResponse(message) {
685
+ const pending = this.pendingRequests.get(message.requestId);
686
+ if (pending) {
687
+ clearTimeout(pending.timeout);
688
+ this.pendingRequests.delete(message.requestId);
689
+ pending.resolve(message);
690
+ }
691
+ }
692
+ handleTcpConnection(tunnel, socket) {
693
+ const connectionId = (0, utils_1.generateId)();
694
+ tunnel.tcpConnections.set(connectionId, socket);
695
+ tunnel.stats.connections++;
696
+ this.logger.info(`TCP connection ${connectionId} to tunnel ${tunnel.id}`);
697
+ socket.on("data", (data) => {
698
+ tunnel.stats.bytesIn += data.length;
699
+ this.send(tunnel.client.ws, {
700
+ type: "tcp_data",
701
+ id: (0, utils_1.generateId)(),
702
+ timestamp: Date.now(),
703
+ tunnelId: tunnel.id,
704
+ connectionId,
705
+ data: (0, utils_1.encodeBase64)(data),
706
+ });
707
+ });
708
+ socket.on("close", () => {
709
+ tunnel.tcpConnections.delete(connectionId);
710
+ this.send(tunnel.client.ws, {
711
+ type: "tcp_close",
712
+ id: (0, utils_1.generateId)(),
713
+ timestamp: Date.now(),
714
+ tunnelId: tunnel.id,
715
+ connectionId,
716
+ });
717
+ });
718
+ socket.on("error", (err) => {
719
+ this.logger.error(`TCP connection error: ${err.message}`);
720
+ });
721
+ }
722
+ handleTcpData(message) {
723
+ // Find tunnel and connection
724
+ for (const client of this.clients.values()) {
725
+ const tunnel = client.tunnels.get(message.tunnelId);
726
+ if (tunnel) {
727
+ const socket = tunnel.tcpConnections.get(message.connectionId);
728
+ if (socket) {
729
+ const data = (0, utils_1.decodeBase64)(message.data);
730
+ tunnel.stats.bytesOut += data.length;
731
+ socket.write(data);
732
+ }
733
+ break;
734
+ }
735
+ }
736
+ }
737
+ handleApiRequest(req, res) {
738
+ const url = new URL(req.url || "/", `http://${req.headers.host}`);
739
+ res.setHeader("Content-Type", "application/json");
740
+ if (url.pathname === "/api/tunnels" && req.method === "GET") {
741
+ const tunnels = this.getAllTunnels();
742
+ res.writeHead(200);
743
+ res.end(JSON.stringify({ tunnels }));
744
+ }
745
+ else if (url.pathname === "/api/stats" && req.method === "GET") {
746
+ res.writeHead(200);
747
+ res.end(JSON.stringify({
748
+ clients: this.clients.size,
749
+ tunnels: this.getTunnelCount(),
750
+ uptime: process.uptime(),
751
+ }));
752
+ }
753
+ else {
754
+ res.writeHead(404);
755
+ res.end(JSON.stringify({ error: "Not found" }));
756
+ }
757
+ }
758
+ send(ws, message) {
759
+ if (ws.readyState === ws_1.WebSocket.OPEN) {
760
+ ws.send((0, utils_1.encodeMessage)(message));
761
+ }
762
+ }
763
+ getTunnelCount() {
764
+ let count = 0;
765
+ for (const client of this.clients.values()) {
766
+ count += client.tunnels.size;
767
+ }
768
+ return count;
769
+ }
770
+ getAllTunnels() {
771
+ const tunnels = [];
772
+ for (const client of this.clients.values()) {
773
+ for (const tunnel of client.tunnels.values()) {
774
+ tunnels.push({
775
+ id: tunnel.id,
776
+ protocol: tunnel.config.protocol,
777
+ localAddress: `${tunnel.config.localHost}:${tunnel.config.localPort}`,
778
+ publicUrl: tunnel.publicUrl,
779
+ createdAt: tunnel.createdAt,
780
+ bytesIn: tunnel.stats.bytesIn,
781
+ bytesOut: tunnel.stats.bytesOut,
782
+ connections: tunnel.stats.connections,
783
+ });
784
+ }
785
+ }
786
+ return tunnels;
787
+ }
788
+ }
789
+ exports.TunnelServer = TunnelServer;
790
+ //# sourceMappingURL=TunnelServer.js.map