pinggy 0.4.9 → 0.5.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.
@@ -1,2857 +0,0 @@
1
- import {
2
- logger
3
- } from "./chunk-3RTRUYNW.js";
4
-
5
- // src/utils/printer.ts
6
- import pico2 from "picocolors";
7
-
8
- // src/tui/spinner/spinner.ts
9
- import pico from "picocolors";
10
- var spinners = {
11
- dots: {
12
- interval: 80,
13
- frames: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"]
14
- }
15
- };
16
- var currentTimer = null;
17
- var currentText = "";
18
- function startSpinner(name = "dots", text = "Loading") {
19
- const spinner = spinners[name];
20
- let i = 0;
21
- currentText = text;
22
- if (currentTimer) {
23
- clearInterval(currentTimer);
24
- }
25
- currentTimer = setInterval(() => {
26
- const frame = spinner.frames[i = ++i % spinner.frames.length];
27
- process.stdout.write(`\r${pico.cyan(frame)} ${text}`);
28
- }, spinner.interval);
29
- return () => stopSpinner();
30
- }
31
- function stopSpinner() {
32
- if (currentTimer) {
33
- clearInterval(currentTimer);
34
- currentTimer = null;
35
- process.stdout.write("\r\x1B[K");
36
- }
37
- }
38
- function stopSpinnerSuccess(message) {
39
- if (currentTimer) {
40
- clearInterval(currentTimer);
41
- currentTimer = null;
42
- const finalMessage = message || currentText;
43
- process.stdout.write(`\r${pico.green("\u2714")} ${finalMessage}
44
- `);
45
- }
46
- }
47
- function stopSpinnerFail(message) {
48
- if (currentTimer) {
49
- clearInterval(currentTimer);
50
- currentTimer = null;
51
- const finalMessage = message || currentText;
52
- process.stdout.write(`\r${pico.red("\u2716")} ${finalMessage}
53
- `);
54
- }
55
- }
56
-
57
- // src/utils/printer.ts
58
- var _CLIPrinter = class _CLIPrinter {
59
- static isCLIError(err) {
60
- return err instanceof Error;
61
- }
62
- static print(message, ...args) {
63
- console.log(message, ...args);
64
- }
65
- static error(err) {
66
- const def = this.errorDefinitions.find((d) => d.match(err));
67
- const msg = def.message(err);
68
- console.error(pico2.red(pico2.bold("\u2716 Error:")), pico2.red(msg));
69
- }
70
- static fatal(err) {
71
- const def = this.errorDefinitions.find((d) => d.match(err));
72
- const msg = def.message(err);
73
- console.error(pico2.red(pico2.bold("\u2716 Fatal Error:")), pico2.red(msg));
74
- process.exit(1);
75
- }
76
- static red(message) {
77
- return pico2.red(message);
78
- }
79
- static warn(message) {
80
- console.warn(pico2.yellow(pico2.bold("\u26A0 Warning:")), pico2.yellow(message));
81
- }
82
- static warnTxt(message) {
83
- console.warn(pico2.yellow(pico2.bold("\u26A0 Warning:")), pico2.yellow(message));
84
- }
85
- static success(message) {
86
- console.log(pico2.green(pico2.bold(" \u2714 Success:")), pico2.green(message));
87
- }
88
- static async info(message) {
89
- console.log(pico2.blue(message));
90
- }
91
- static startSpinner(message) {
92
- startSpinner("dots", message);
93
- }
94
- static stopSpinnerSuccess(message) {
95
- stopSpinnerSuccess(message);
96
- }
97
- static stopSpinnerFail(message) {
98
- stopSpinnerFail(message);
99
- }
100
- };
101
- _CLIPrinter.errorDefinitions = [
102
- {
103
- match: (err) => _CLIPrinter.isCLIError(err) && err.code === "ERR_PARSE_ARGS_UNKNOWN_OPTION",
104
- message: (err) => {
105
- const match = /Unknown option '(.+?)'/.exec(err.message);
106
- const option = match ? match[1] : "(unknown)";
107
- return `Unknown option '${option}'. Please check your command or use pinggy -h for guidance.`;
108
- }
109
- },
110
- {
111
- match: (err) => _CLIPrinter.isCLIError(err) && err.code === "ERR_PARSE_ARGS_MISSING_OPTION_VALUE",
112
- message: (err) => `Missing required argument for option '${err.option}'.`
113
- },
114
- {
115
- match: (err) => _CLIPrinter.isCLIError(err) && err.code === "ERR_PARSE_ARGS_INVALID_OPTION_VALUE",
116
- message: (err) => `Invalid argument'${err.message}'.`
117
- },
118
- {
119
- match: (err) => _CLIPrinter.isCLIError(err) && err.code === "ENOENT",
120
- message: (err) => `File or directory not found: ${err.message}`
121
- },
122
- {
123
- match: () => true,
124
- // fallback
125
- message: (err) => _CLIPrinter.isCLIError(err) ? err.message : String(err)
126
- }
127
- ];
128
- var CLIPrinter = _CLIPrinter;
129
- var printer_default = CLIPrinter;
130
-
131
- // src/utils/util.ts
132
- import { readFileSync } from "fs";
133
- import { randomUUID } from "crypto";
134
- import { fileURLToPath } from "url";
135
- import { dirname, join } from "path";
136
- function getRandomId() {
137
- return randomUUID();
138
- }
139
- function isValidPort(p) {
140
- return Number.isInteger(p) && p > 0 && p < 65536;
141
- }
142
- var __filename2 = fileURLToPath(import.meta.url);
143
- var __dirname2 = dirname(__filename2);
144
- function getVersion() {
145
- try {
146
- const packageJsonPath = join(__dirname2, "../package.json");
147
- const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
148
- return pkg.version ?? "";
149
- } catch (error) {
150
- printer_default.error("Error reading version info");
151
- return "";
152
- }
153
- }
154
-
155
- // src/tunnel_manager/TunnelManager.ts
156
- import { pinggy } from "@pinggy/pinggy";
157
- import path from "path";
158
- import { Worker } from "worker_threads";
159
- import { fileURLToPath as fileURLToPath2 } from "url";
160
- var __filename3 = fileURLToPath2(import.meta.url);
161
- var __dirname3 = path.dirname(__filename3);
162
- var TunnelManager = class _TunnelManager {
163
- constructor() {
164
- this.tunnelsByTunnelId = /* @__PURE__ */ new Map();
165
- this.tunnelsByConfigId = /* @__PURE__ */ new Map();
166
- this.tunnelStats = /* @__PURE__ */ new Map();
167
- this.tunnelStatsListeners = /* @__PURE__ */ new Map();
168
- this.tunnelErrorListeners = /* @__PURE__ */ new Map();
169
- this.tunnelPollingErrorListeners = /* @__PURE__ */ new Map();
170
- this.tunnelDisconnectListeners = /* @__PURE__ */ new Map();
171
- this.tunnelWorkerErrorListeners = /* @__PURE__ */ new Map();
172
- this.tunnelStartListeners = /* @__PURE__ */ new Map();
173
- this.tunnelWillReconnectListeners = /* @__PURE__ */ new Map();
174
- this.tunnelReconnectingListeners = /* @__PURE__ */ new Map();
175
- this.tunnelReconnectionCompletedListeners = /* @__PURE__ */ new Map();
176
- this.tunnelReconnectionFailedListeners = /* @__PURE__ */ new Map();
177
- }
178
- static getInstance() {
179
- if (!_TunnelManager.instance) {
180
- _TunnelManager.instance = new _TunnelManager();
181
- }
182
- return _TunnelManager.instance;
183
- }
184
- /**
185
- * Creates a new managed tunnel instance with the given configuration.
186
- * Optionally builds the config with forwarding rules based on buildConfig flag.
187
- *
188
- * @param config - The tunnel configuration options
189
- *
190
- * @throws {Error} When configId is invalid or empty
191
- * @throws {Error} When a tunnel with the given configId already exists
192
- *
193
- * @returns {ManagedTunnel} A new managed tunnel instance containing the tunnel details,
194
- * status information, and statistics
195
- */
196
- async createTunnel(config) {
197
- const { configId, tunnelid: requestedTunnelId, tunnelName, name } = config;
198
- const tunnelid = requestedTunnelId || getRandomId();
199
- const autoReconnect = config.autoReconnect || false;
200
- const serve = this.resolveServePath(config);
201
- if (!configId || typeof configId !== "string" || configId.trim() === "") {
202
- throw new Error("configId is required and must be a non-empty string");
203
- }
204
- return this._createTunnelWithProcessedConfig({
205
- configId,
206
- tunnelid,
207
- tunnelName: tunnelName || name,
208
- originalConfig: config,
209
- serve,
210
- autoReconnect
211
- });
212
- }
213
- /**
214
- * Internal method to create a tunnel with an already-processed configuration.
215
- * This is used by createTunnel, restartTunnel, and updateConfig to avoid config processing.
216
- *
217
- * @param params - Configuration parameters with already-processed forwarding rules
218
- * @returns The created ManagedTunnel instance
219
- * @private
220
- */
221
- async _createTunnelWithProcessedConfig(params) {
222
- let instance;
223
- try {
224
- logger.debug("Creating tunnel instance with processed config", params.originalConfig);
225
- instance = await pinggy.createTunnel(params.originalConfig);
226
- } catch (e) {
227
- logger.error("Error creating tunnel instance:", e);
228
- throw e;
229
- }
230
- const now = (/* @__PURE__ */ new Date()).toISOString();
231
- const managed = {
232
- tunnelid: params.tunnelid,
233
- configId: params.configId,
234
- tunnelName: params.tunnelName,
235
- instance,
236
- tunnelConfig: params.originalConfig,
237
- serve: params.serve,
238
- warnings: [],
239
- isStopped: false,
240
- createdAt: now,
241
- startedAt: null,
242
- stoppedAt: null,
243
- autoReconnect: params.autoReconnect,
244
- lastError: {}
245
- };
246
- instance.setTunnelEstablishedCallback(({}) => {
247
- managed.startedAt = (/* @__PURE__ */ new Date()).toISOString();
248
- });
249
- this.setupStatsCallback(params.tunnelid, managed);
250
- this.setupErrorCallback(params.tunnelid, managed);
251
- this.setupTunnelPollingErrorCallback(params.tunnelid, managed);
252
- this.setupDisconnectCallback(params.tunnelid, managed);
253
- this.setupWillReconnectCallback(params.tunnelid, managed);
254
- this.setupReconnectingCallback(params.tunnelid, managed);
255
- this.setupReconnectionCompletedCallback(params.tunnelid, managed);
256
- this.setupReconnectionFailedCallback(params.tunnelid, managed);
257
- this.setUpTunnelWorkerErrorCallback(params.tunnelid, managed);
258
- this.tunnelsByTunnelId.set(params.tunnelid, managed);
259
- this.tunnelsByConfigId.set(params.configId, managed);
260
- logger.info("Tunnel created", { configId: params.configId, tunnelId: params.tunnelid });
261
- return managed;
262
- }
263
- /**
264
- * Start a tunnel that was created but not yet started
265
- */
266
- async startTunnel(tunnelId) {
267
- const managed = this.tunnelsByTunnelId.get(tunnelId);
268
- if (!managed) {
269
- throw new Error(`Tunnel with id "${tunnelId}" not found`);
270
- }
271
- logger.info("Starting tunnel", { tunnelId });
272
- let urls;
273
- try {
274
- urls = await managed.instance.start();
275
- } catch (error) {
276
- logger.warn("Failed to start tunnel", { tunnelId, error });
277
- managed.isStopped = true;
278
- managed.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
279
- managed.lastError = {
280
- message: "Failed to start tunnel",
281
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
282
- isFatal: true
283
- };
284
- throw error;
285
- }
286
- logger.info("Tunnel started", { tunnelId, urls });
287
- logger.info("Checking serve config for tunnel", { tunnelId, serve: managed.serve });
288
- if (managed.serve) {
289
- this.startStaticFileServer(managed);
290
- } else {
291
- logger.debug("No serve path configured, skipping static file server", { tunnelId });
292
- }
293
- try {
294
- const startListeners = this.tunnelStartListeners.get(tunnelId);
295
- if (startListeners) {
296
- for (const [id, listener] of startListeners) {
297
- try {
298
- listener(tunnelId, urls);
299
- } catch (err) {
300
- logger.debug("Error in start-listener callback", { listenerId: id, tunnelId, err });
301
- }
302
- }
303
- }
304
- } catch (e) {
305
- logger.warn("Failed to notify start listeners", { tunnelId, e });
306
- }
307
- return urls;
308
- }
309
- /**
310
- * Stops a running tunnel and updates its status.
311
- *
312
- * @param tunnelId - The unique identifier of the tunnel to stop
313
- * @throws {Error} If the tunnel with the given tunnelId is not found
314
- * @remarks
315
- * - Clears the tunnel's remote URLs
316
- * - Updates the tunnel's state to Exited if stopped successfully
317
- * - Logs the stop operation with tunnelId and configId
318
- */
319
- stopTunnel(tunnelId) {
320
- const managed = this.tunnelsByTunnelId.get(tunnelId);
321
- if (!managed) {
322
- throw new Error(`Tunnel "${tunnelId}" not found`);
323
- }
324
- logger.info("Stopping tunnel", { tunnelId, configId: managed.configId });
325
- try {
326
- managed.instance.stop();
327
- if (managed.serveWorker) {
328
- logger.info("terminating serveWorker");
329
- managed.serveWorker.terminate();
330
- }
331
- this.tunnelStats.delete(tunnelId);
332
- this.tunnelStatsListeners.delete(tunnelId);
333
- this.tunnelStats.delete(tunnelId);
334
- this.tunnelStatsListeners.delete(tunnelId);
335
- this.tunnelErrorListeners.delete(tunnelId);
336
- this.tunnelPollingErrorListeners.delete(tunnelId);
337
- this.tunnelDisconnectListeners.delete(tunnelId);
338
- this.tunnelWorkerErrorListeners.delete(tunnelId);
339
- this.tunnelStartListeners.delete(tunnelId);
340
- this.tunnelWillReconnectListeners.delete(tunnelId);
341
- this.tunnelReconnectingListeners.delete(tunnelId);
342
- this.tunnelReconnectionCompletedListeners.delete(tunnelId);
343
- this.tunnelReconnectionFailedListeners.delete(tunnelId);
344
- managed.serveWorker = null;
345
- managed.warnings = managed.warnings ?? [];
346
- managed.isStopped = true;
347
- managed.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
348
- logger.info("Tunnel stopped", { tunnelId, configId: managed.configId });
349
- return { configId: managed.configId, tunnelid: managed.tunnelid };
350
- } catch (error) {
351
- logger.error("Failed to stop tunnel", { tunnelId, error });
352
- throw error;
353
- }
354
- }
355
- /**
356
- * Get all public URLs for a tunnel
357
- */
358
- async getTunnelUrls(tunnelId) {
359
- try {
360
- const managed = this.tunnelsByTunnelId.get(tunnelId);
361
- if (!managed || managed.isStopped) {
362
- logger.error(`Tunnel "${tunnelId}" not found when fetching URLs`);
363
- return [];
364
- }
365
- const urls = await managed.instance.urls();
366
- return urls;
367
- } catch (error) {
368
- logger.error("Error fetching tunnel URLs", { tunnelId, error });
369
- throw error;
370
- }
371
- }
372
- /**
373
- * Get all TunnelStatus currently managed by this TunnelManager
374
- * @returns An array of all TunnelStatus objects
375
- */
376
- async getAllTunnels() {
377
- try {
378
- const tunnelList = await Promise.all(Array.from(this.tunnelsByTunnelId.values()).map(async (tunnel) => {
379
- return {
380
- tunnelid: tunnel.tunnelid,
381
- configId: tunnel.configId,
382
- tunnelName: tunnel.tunnelName,
383
- tunnelConfig: tunnel.tunnelConfig,
384
- remoteurls: tunnel.isStopped || tunnel.lastError?.isFatal ? [] : await this.getTunnelUrls(tunnel.tunnelid),
385
- serve: tunnel.serve
386
- };
387
- }));
388
- return tunnelList;
389
- } catch (err) {
390
- logger.error("Error fetching tunnels", { error: err });
391
- return [];
392
- }
393
- }
394
- /**
395
- * Get status of a tunnel
396
- */
397
- async getTunnelStatus(tunnelId) {
398
- const managed = this.tunnelsByTunnelId.get(tunnelId);
399
- if (!managed) {
400
- logger.error(`Tunnel "${tunnelId}" not found when fetching status`);
401
- throw new Error(`Tunnel "${tunnelId}" not found`);
402
- }
403
- if (managed.isStopped) {
404
- return "exited";
405
- }
406
- const status = await managed.instance.getStatus();
407
- return status;
408
- }
409
- /**
410
- * Stop all tunnels
411
- */
412
- stopAllTunnels() {
413
- for (const { instance } of this.tunnelsByTunnelId.values()) {
414
- try {
415
- instance.stop();
416
- } catch (e) {
417
- logger.warn("Error stopping tunnel instance", e);
418
- }
419
- }
420
- this.tunnelsByTunnelId.clear();
421
- this.tunnelsByConfigId.clear();
422
- this.tunnelStats.clear();
423
- this.tunnelStatsListeners.clear();
424
- this.tunnelErrorListeners.clear();
425
- this.tunnelPollingErrorListeners.clear();
426
- this.tunnelDisconnectListeners.clear();
427
- this.tunnelWorkerErrorListeners.clear();
428
- this.tunnelStartListeners.clear();
429
- this.tunnelWillReconnectListeners.clear();
430
- this.tunnelReconnectingListeners.clear();
431
- this.tunnelReconnectionCompletedListeners.clear();
432
- this.tunnelReconnectionFailedListeners.clear();
433
- logger.info("All tunnels stopped and cleared");
434
- }
435
- /**
436
- * Remove a stopped tunnel's records so it will no longer be returned by list methods.
437
- *
438
- *
439
- * @param tunnelId - the tunnel id to remove
440
- * @returns true if the record was removed, false otherwise
441
- */
442
- removeStoppedTunnelByTunnelId(tunnelId) {
443
- const managed = this.tunnelsByTunnelId.get(tunnelId);
444
- if (!managed) {
445
- logger.debug("Attempted to remove non-existent tunnel", { tunnelId });
446
- return false;
447
- }
448
- if (!managed.isStopped) {
449
- logger.warn("Attempted to remove tunnel that is not stopped", { tunnelId });
450
- return false;
451
- }
452
- this._cleanupTunnelRecords(managed);
453
- logger.info("Removed stopped tunnel records", { tunnelId, configId: managed.configId });
454
- return true;
455
- }
456
- /**
457
- * Remove a stopped tunnel by its config id.
458
- * @param configId - the config id to remove
459
- * @returns true if the record was removed, false otherwise
460
- */
461
- removeStoppedTunnelByConfigId(configId) {
462
- const managed = this.tunnelsByConfigId.get(configId);
463
- if (!managed) {
464
- logger.debug("Attempted to remove non-existent tunnel by configId", { configId });
465
- return false;
466
- }
467
- return this.removeStoppedTunnelByTunnelId(managed.tunnelid);
468
- }
469
- _cleanupTunnelRecords(managed) {
470
- if (!managed.isStopped) {
471
- throw new Error(`Active tunnel "${managed.tunnelid}" cannot be removed`);
472
- }
473
- try {
474
- if (managed.serveWorker) {
475
- managed.serveWorker = null;
476
- }
477
- this.tunnelStats.delete(managed.tunnelid);
478
- this.tunnelStatsListeners.delete(managed.tunnelid);
479
- this.tunnelErrorListeners.delete(managed.tunnelid);
480
- this.tunnelPollingErrorListeners.delete(managed.tunnelid);
481
- this.tunnelDisconnectListeners.delete(managed.tunnelid);
482
- this.tunnelWorkerErrorListeners.delete(managed.tunnelid);
483
- this.tunnelStartListeners.delete(managed.tunnelid);
484
- this.tunnelWillReconnectListeners.delete(managed.tunnelid);
485
- this.tunnelReconnectingListeners.delete(managed.tunnelid);
486
- this.tunnelReconnectionCompletedListeners.delete(managed.tunnelid);
487
- this.tunnelReconnectionFailedListeners.delete(managed.tunnelid);
488
- this.tunnelsByTunnelId.delete(managed.tunnelid);
489
- this.tunnelsByConfigId.delete(managed.configId);
490
- } catch (e) {
491
- logger.warn("Failed cleaning up tunnel records", { tunnelId: managed.tunnelid, error: e });
492
- }
493
- }
494
- /**
495
- * Get tunnel instance by either configId or tunnelId
496
- * @param configId - The configuration ID of the tunnel
497
- * @param tunnelId - The tunnel ID
498
- * @returns The tunnel instance
499
- * @throws Error if neither configId nor tunnelId is provided, or if tunnel is not found
500
- */
501
- getTunnelInstance(configId, tunnelId) {
502
- if (configId) {
503
- const managed = this.tunnelsByConfigId.get(configId);
504
- if (!managed) {
505
- throw new Error(`Tunnel "${configId}" not found`);
506
- }
507
- return managed.instance;
508
- }
509
- if (tunnelId) {
510
- const managed = this.tunnelsByTunnelId.get(tunnelId);
511
- if (!managed) {
512
- throw new Error(`Tunnel "${tunnelId}" not found`);
513
- }
514
- return managed.instance;
515
- }
516
- throw new Error(`Either configId or tunnelId must be provided`);
517
- }
518
- /**
519
- * Get tunnel config by either configId or tunnelId
520
- * @param configId - The configuration ID of the tunnel
521
- * @param tunnelId - The tunnel ID
522
- * @returns The tunnel config
523
- * @throws Error if neither configId nor tunnelId is provided, or if tunnel is not found
524
- */
525
- async getTunnelConfig(configId, tunnelId) {
526
- if (configId) {
527
- const managed = this.tunnelsByConfigId.get(configId);
528
- if (!managed) {
529
- throw new Error(`Tunnel with configId "${configId}" not found`);
530
- }
531
- return managed.instance.getConfig();
532
- }
533
- if (tunnelId) {
534
- const managed = this.tunnelsByTunnelId.get(tunnelId);
535
- if (!managed) {
536
- throw new Error(`Tunnel with tunnelId "${tunnelId}" not found`);
537
- }
538
- return managed.instance.getConfig();
539
- }
540
- throw new Error(`Either configId or tunnelId must be provided`);
541
- }
542
- /**
543
- * Restarts a tunnel with its current configuration.
544
- * This function will stop the tunnel if it's running and start it again.
545
- * All configurations including additional forwarding rules are preserved.
546
- */
547
- async restartTunnel(tunnelid) {
548
- const existingTunnel = this.tunnelsByTunnelId.get(tunnelid);
549
- if (!existingTunnel) {
550
- throw new Error(`Tunnel "${tunnelid}" not found`);
551
- }
552
- logger.info("Initiating tunnel restart", {
553
- tunnelId: tunnelid,
554
- configId: existingTunnel.configId
555
- });
556
- try {
557
- const tunnelName = existingTunnel.tunnelName;
558
- const currentConfigId = existingTunnel.configId;
559
- const currentConfig = existingTunnel.tunnelConfig;
560
- const currentServe = existingTunnel.serve;
561
- const autoReconnect = existingTunnel.autoReconnect || false;
562
- this.tunnelsByTunnelId.delete(tunnelid);
563
- this.tunnelsByConfigId.delete(existingTunnel.configId);
564
- this.tunnelStats.delete(tunnelid);
565
- this.tunnelStatsListeners.delete(tunnelid);
566
- this.tunnelErrorListeners.delete(tunnelid);
567
- this.tunnelPollingErrorListeners.delete(tunnelid);
568
- this.tunnelDisconnectListeners.delete(tunnelid);
569
- this.tunnelWorkerErrorListeners.delete(tunnelid);
570
- this.tunnelStartListeners.delete(tunnelid);
571
- this.tunnelWillReconnectListeners.delete(tunnelid);
572
- this.tunnelReconnectingListeners.delete(tunnelid);
573
- this.tunnelReconnectionCompletedListeners.delete(tunnelid);
574
- this.tunnelReconnectionFailedListeners.delete(tunnelid);
575
- const newTunnel = await this._createTunnelWithProcessedConfig({
576
- configId: currentConfigId,
577
- tunnelid,
578
- tunnelName,
579
- originalConfig: currentConfig,
580
- serve: currentServe,
581
- autoReconnect
582
- });
583
- if (existingTunnel.createdAt) {
584
- newTunnel.createdAt = existingTunnel.createdAt;
585
- }
586
- await this.startTunnel(newTunnel.tunnelid);
587
- } catch (error) {
588
- logger.error("Failed to restart tunnel", {
589
- tunnelid,
590
- error: error instanceof Error ? error.message : "Unknown error"
591
- });
592
- throw new Error(`Failed to restart tunnel: ${error instanceof Error ? error.message : "Unknown error"}`);
593
- }
594
- }
595
- /**
596
- * Updates the configuration of an existing tunnel.
597
- *
598
- * This method handles the process of updating a tunnel's configuration while preserving
599
- * its state. If the tunnel is running, it will be stopped, updated, and restarted.
600
- * In case of failure, it attempts to restore the original configuration.
601
- *
602
- * @param newConfig - The new configuration to apply, including configid and optional additional forwarding
603
- *
604
- * @returns Promise resolving to the updated ManagedTunnel
605
- * @throws Error if the tunnel is not found or if the update process fails
606
- */
607
- async updateConfig(newConfig) {
608
- const { configId, tunnelName: newTunnelName } = newConfig;
609
- if (!configId || configId.trim().length === 0) {
610
- throw new Error(`Invalid configId: "${configId}"`);
611
- }
612
- const existingTunnel = this.tunnelsByConfigId.get(configId);
613
- if (!existingTunnel) {
614
- throw new Error(`Tunnel with config id "${configId}" not found`);
615
- }
616
- const isStopped = existingTunnel.isStopped;
617
- const currentTunnelConfig = existingTunnel.tunnelConfig;
618
- const currentTunnelId = existingTunnel.tunnelid;
619
- const currentTunnelConfigId = existingTunnel.configId;
620
- const currentTunnelName = existingTunnel.tunnelName;
621
- const currentServe = existingTunnel.serve;
622
- const currentAutoReconnect = existingTunnel.autoReconnect || false;
623
- const requestedServe = this.resolveServePath(newConfig);
624
- try {
625
- if (!isStopped) {
626
- existingTunnel.instance.stop();
627
- }
628
- this.tunnelsByTunnelId.delete(currentTunnelId);
629
- this.tunnelsByConfigId.delete(currentTunnelConfigId);
630
- const mergedBaseConfig = {
631
- ...newConfig,
632
- configId,
633
- tunnelName: newTunnelName !== void 0 ? newTunnelName : currentTunnelName,
634
- serve: requestedServe !== void 0 ? requestedServe : currentServe
635
- };
636
- const effectiveServe = requestedServe !== void 0 ? requestedServe : currentServe;
637
- const effectiveTunnelName = newTunnelName !== void 0 ? newTunnelName : currentTunnelName;
638
- let configWithForwarding;
639
- const newTunnel = await this._createTunnelWithProcessedConfig({
640
- configId,
641
- tunnelid: currentTunnelId,
642
- tunnelName: effectiveTunnelName,
643
- originalConfig: mergedBaseConfig,
644
- serve: effectiveServe,
645
- autoReconnect: currentAutoReconnect
646
- });
647
- if (!isStopped) {
648
- await this.startTunnel(newTunnel.tunnelid);
649
- }
650
- logger.info("Tunnel configuration updated", {
651
- tunnelId: newTunnel.tunnelid,
652
- configId: newTunnel.configId,
653
- isStopped
654
- });
655
- return newTunnel;
656
- } catch (error) {
657
- logger.error("Error updating tunnel configuration", {
658
- configId,
659
- error: error instanceof Error ? error.message : String(error)
660
- });
661
- try {
662
- const originalTunnel = await this._createTunnelWithProcessedConfig({
663
- configId: currentTunnelConfigId,
664
- tunnelid: currentTunnelId,
665
- tunnelName: currentTunnelName,
666
- originalConfig: currentTunnelConfig,
667
- serve: currentServe,
668
- autoReconnect: currentAutoReconnect
669
- });
670
- if (!isStopped) {
671
- await this.startTunnel(originalTunnel.tunnelid);
672
- }
673
- logger.warn("Restored original tunnel configuration after update failure", {
674
- currentTunnelId,
675
- error: error instanceof Error ? error.message : "Unknown error"
676
- });
677
- } catch (restoreError) {
678
- logger.error("Failed to restore original tunnel configuration", {
679
- currentTunnelId,
680
- error: restoreError instanceof Error ? restoreError.message : "Unknown error"
681
- });
682
- }
683
- throw error;
684
- }
685
- }
686
- /**
687
- * Retrieve the ManagedTunnel object by either configId or tunnelId.
688
- * Throws an error if neither id is provided or the tunnel is not found.
689
- */
690
- getManagedTunnel(configId, tunnelId) {
691
- if (configId) {
692
- const managed = this.tunnelsByConfigId.get(configId);
693
- if (!managed) {
694
- throw new Error(`Tunnel "${configId}" not found`);
695
- }
696
- return managed;
697
- }
698
- if (tunnelId) {
699
- const managed = this.tunnelsByTunnelId.get(tunnelId);
700
- if (!managed) {
701
- throw new Error(`Tunnel "${tunnelId}" not found`);
702
- }
703
- return managed;
704
- }
705
- throw new Error(`Either configId or tunnelId must be provided`);
706
- }
707
- async getTunnelGreetMessage(tunnelId) {
708
- const managed = this.tunnelsByTunnelId.get(tunnelId);
709
- if (!managed) {
710
- logger.error(`Tunnel "${tunnelId}" not found when fetching greet message`);
711
- return null;
712
- }
713
- try {
714
- if (managed.isStopped) {
715
- return null;
716
- }
717
- const messages = await managed.instance.getGreetMessage();
718
- if (Array.isArray(messages)) {
719
- return messages.join(" ");
720
- }
721
- return messages ?? null;
722
- } catch (e) {
723
- logger.error(
724
- `Error fetching greet message for tunnel "${tunnelId}": ${e instanceof Error ? e.message : String(e)}`
725
- );
726
- return null;
727
- }
728
- }
729
- getTunnelStats(tunnelId) {
730
- const managed = this.tunnelsByTunnelId.get(tunnelId);
731
- if (!managed) {
732
- return null;
733
- }
734
- const stats = this.tunnelStats.get(tunnelId);
735
- return stats || null;
736
- }
737
- getLatestTunnelStats(tunnelId) {
738
- const managed = this.tunnelsByTunnelId.get(tunnelId);
739
- if (!managed) {
740
- return null;
741
- }
742
- const stats = this.tunnelStats.get(tunnelId);
743
- if (stats && stats.length > 0) {
744
- return stats[stats.length - 1];
745
- }
746
- return null;
747
- }
748
- /**
749
- * Registers a listener function to receive tunnel statistics updates.
750
- * The listener will be called whenever any tunnel's stats are updated.
751
- *
752
- * @param tunnelId - The tunnel ID to listen to stats for
753
- * @param listener - Function that receives tunnelId and stats when updates occur
754
- * @returns A unique listener ID that can be used to deregister the listener and tunnelId
755
- *
756
- * @throws {Error} When the specified tunnelId does not exist
757
- */
758
- async registerStatsListener(tunnelId, listener) {
759
- const managed = this.tunnelsByTunnelId.get(tunnelId);
760
- if (!managed) {
761
- throw new Error(`Tunnel "${tunnelId}" not found`);
762
- }
763
- if (!this.tunnelStatsListeners.has(tunnelId)) {
764
- this.tunnelStatsListeners.set(tunnelId, /* @__PURE__ */ new Map());
765
- }
766
- const listenerId = getRandomId();
767
- const tunnelListeners = this.tunnelStatsListeners.get(tunnelId);
768
- tunnelListeners.set(listenerId, listener);
769
- logger.info("Stats listener registered for tunnel", { tunnelId, listenerId });
770
- return [listenerId, tunnelId];
771
- }
772
- async registerErrorListener(tunnelId, listener) {
773
- const managed = this.tunnelsByTunnelId.get(tunnelId);
774
- if (!managed) {
775
- throw new Error(`Tunnel "${tunnelId}" not found`);
776
- }
777
- if (!this.tunnelErrorListeners.has(tunnelId)) {
778
- this.tunnelErrorListeners.set(tunnelId, /* @__PURE__ */ new Map());
779
- }
780
- const listenerId = getRandomId();
781
- const tunnelErrorListeners = this.tunnelErrorListeners.get(tunnelId);
782
- tunnelErrorListeners.set(listenerId, listener);
783
- logger.info("Error listener registered for tunnel", { tunnelId, listenerId });
784
- return listenerId;
785
- }
786
- async registerPollingErrorListener(tunnelId, listener) {
787
- const managed = this.tunnelsByTunnelId.get(tunnelId);
788
- if (!managed) {
789
- throw new Error(`Tunnel "${tunnelId}" not found`);
790
- }
791
- if (!this.tunnelPollingErrorListeners.has(tunnelId)) {
792
- this.tunnelPollingErrorListeners.set(tunnelId, /* @__PURE__ */ new Map());
793
- }
794
- const listenerId = getRandomId();
795
- this.tunnelPollingErrorListeners.get(tunnelId).set(listenerId, listener);
796
- logger.info("Polling error listener registered for tunnel", { tunnelId, listenerId });
797
- return listenerId;
798
- }
799
- async registerDisconnectListener(tunnelId, listener) {
800
- const managed = this.tunnelsByTunnelId.get(tunnelId);
801
- if (!managed) {
802
- throw new Error(`Tunnel "${tunnelId}" not found`);
803
- }
804
- if (!this.tunnelDisconnectListeners.has(tunnelId)) {
805
- this.tunnelDisconnectListeners.set(tunnelId, /* @__PURE__ */ new Map());
806
- }
807
- const listenerId = getRandomId();
808
- const tunnelDisconnectListeners = this.tunnelDisconnectListeners.get(tunnelId);
809
- tunnelDisconnectListeners.set(listenerId, listener);
810
- logger.info("Disconnect listener registered for tunnel", { tunnelId, listenerId });
811
- return listenerId;
812
- }
813
- async registerWorkerErrorListner(tunnelId, listener) {
814
- const managed = this.tunnelsByTunnelId.get(tunnelId);
815
- if (!managed) {
816
- throw new Error(`Tunnel "${tunnelId}" not found`);
817
- }
818
- if (!this.tunnelWorkerErrorListeners.has(tunnelId)) {
819
- this.tunnelWorkerErrorListeners.set(tunnelId, /* @__PURE__ */ new Map());
820
- }
821
- const listenerId = getRandomId();
822
- const tunnelWorkerErrorListner = this.tunnelWorkerErrorListeners.get(tunnelId);
823
- tunnelWorkerErrorListner?.set(listenerId, listener);
824
- logger.info("TunnelWorker error listener registered for tunnel", { tunnelId, listenerId });
825
- }
826
- async registerStartListener(tunnelId, listener) {
827
- const managed = this.tunnelsByTunnelId.get(tunnelId);
828
- if (!managed) {
829
- throw new Error(`Tunnel "${tunnelId}" not found`);
830
- }
831
- if (!this.tunnelStartListeners.has(tunnelId)) {
832
- this.tunnelStartListeners.set(tunnelId, /* @__PURE__ */ new Map());
833
- }
834
- const listenerId = getRandomId();
835
- const listeners = this.tunnelStartListeners.get(tunnelId);
836
- listeners.set(listenerId, listener);
837
- logger.info("Start listener registered for tunnel", { tunnelId, listenerId });
838
- return listenerId;
839
- }
840
- async registerWillReconnectListener(tunnelId, listener) {
841
- const managed = this.tunnelsByTunnelId.get(tunnelId);
842
- if (!managed) {
843
- throw new Error(`Tunnel "${tunnelId}" not found`);
844
- }
845
- if (!this.tunnelWillReconnectListeners.has(tunnelId)) {
846
- this.tunnelWillReconnectListeners.set(tunnelId, /* @__PURE__ */ new Map());
847
- }
848
- const listenerId = getRandomId();
849
- this.tunnelWillReconnectListeners.get(tunnelId).set(listenerId, listener);
850
- logger.info("WillReconnect listener registered for tunnel", { tunnelId, listenerId });
851
- return listenerId;
852
- }
853
- async registerReconnectingListener(tunnelId, listener) {
854
- const managed = this.tunnelsByTunnelId.get(tunnelId);
855
- if (!managed) {
856
- throw new Error(`Tunnel "${tunnelId}" not found`);
857
- }
858
- if (!this.tunnelReconnectingListeners.has(tunnelId)) {
859
- this.tunnelReconnectingListeners.set(tunnelId, /* @__PURE__ */ new Map());
860
- }
861
- const listenerId = getRandomId();
862
- this.tunnelReconnectingListeners.get(tunnelId).set(listenerId, listener);
863
- logger.info("Reconnecting listener registered for tunnel", { tunnelId, listenerId });
864
- return listenerId;
865
- }
866
- async registerReconnectionCompletedListener(tunnelId, listener) {
867
- const managed = this.tunnelsByTunnelId.get(tunnelId);
868
- if (!managed) {
869
- throw new Error(`Tunnel "${tunnelId}" not found`);
870
- }
871
- if (!this.tunnelReconnectionCompletedListeners.has(tunnelId)) {
872
- this.tunnelReconnectionCompletedListeners.set(tunnelId, /* @__PURE__ */ new Map());
873
- }
874
- const listenerId = getRandomId();
875
- this.tunnelReconnectionCompletedListeners.get(tunnelId).set(listenerId, listener);
876
- logger.info("ReconnectionCompleted listener registered for tunnel", { tunnelId, listenerId });
877
- return listenerId;
878
- }
879
- async registerReconnectionFailedListener(tunnelId, listener) {
880
- const managed = this.tunnelsByTunnelId.get(tunnelId);
881
- if (!managed) {
882
- throw new Error(`Tunnel "${tunnelId}" not found`);
883
- }
884
- if (!this.tunnelReconnectionFailedListeners.has(tunnelId)) {
885
- this.tunnelReconnectionFailedListeners.set(tunnelId, /* @__PURE__ */ new Map());
886
- }
887
- const listenerId = getRandomId();
888
- this.tunnelReconnectionFailedListeners.get(tunnelId).set(listenerId, listener);
889
- logger.info("ReconnectionFailed listener registered for tunnel", { tunnelId, listenerId });
890
- return listenerId;
891
- }
892
- /**
893
- * Removes a previously registered stats listener.
894
- *
895
- * @param tunnelId - The tunnel ID the listener was registered for
896
- * @param listenerId - The unique ID returned when the listener was registered
897
- */
898
- deregisterStatsListener(tunnelId, listenerId) {
899
- const tunnelListeners = this.tunnelStatsListeners.get(tunnelId);
900
- if (!tunnelListeners) {
901
- logger.warn("No listeners found for tunnel", { tunnelId });
902
- return;
903
- }
904
- const removed = tunnelListeners.delete(listenerId);
905
- if (removed) {
906
- logger.info("Stats listener deregistered", { tunnelId, listenerId });
907
- if (tunnelListeners.size === 0) {
908
- this.tunnelStatsListeners.delete(tunnelId);
909
- }
910
- } else {
911
- logger.warn("Attempted to deregister non-existent stats listener", { tunnelId, listenerId });
912
- }
913
- }
914
- deregisterErrorListener(tunnelId, listenerId) {
915
- const listeners = this.tunnelErrorListeners.get(tunnelId);
916
- if (!listeners) {
917
- logger.warn("No error listeners found for tunnel", { tunnelId });
918
- return;
919
- }
920
- const removed = listeners.delete(listenerId);
921
- if (removed) {
922
- logger.info("Error listener deregistered", { tunnelId, listenerId });
923
- if (listeners.size === 0) {
924
- this.tunnelErrorListeners.delete(tunnelId);
925
- }
926
- } else {
927
- logger.warn("Attempted to deregister non-existent error listener", { tunnelId, listenerId });
928
- }
929
- }
930
- deregisterPollingErrorListener(tunnelId, listenerId) {
931
- const listeners = this.tunnelPollingErrorListeners.get(tunnelId);
932
- if (!listeners) {
933
- logger.warn("No polling error listeners found for tunnel", { tunnelId });
934
- return;
935
- }
936
- const removed = listeners.delete(listenerId);
937
- if (removed) {
938
- logger.info("Polling error listener deregistered", { tunnelId, listenerId });
939
- if (listeners.size === 0) {
940
- this.tunnelPollingErrorListeners.delete(tunnelId);
941
- }
942
- } else {
943
- logger.warn("Attempted to deregister non-existent polling error listener", { tunnelId, listenerId });
944
- }
945
- }
946
- deregisterDisconnectListener(tunnelId, listenerId) {
947
- const listeners = this.tunnelDisconnectListeners.get(tunnelId);
948
- if (!listeners) {
949
- logger.warn("No disconnect listeners found for tunnel", { tunnelId });
950
- return;
951
- }
952
- const removed = listeners.delete(listenerId);
953
- if (removed) {
954
- logger.info("Disconnect listener deregistered", { tunnelId, listenerId });
955
- if (listeners.size === 0) {
956
- this.tunnelDisconnectListeners.delete(tunnelId);
957
- }
958
- } else {
959
- logger.warn("Attempted to deregister non-existent disconnect listener", { tunnelId, listenerId });
960
- }
961
- }
962
- deregisterWillReconnectListener(tunnelId, listenerId) {
963
- const listeners = this.tunnelWillReconnectListeners.get(tunnelId);
964
- if (!listeners) {
965
- logger.warn("No will-reconnect listeners found for tunnel", { tunnelId });
966
- return;
967
- }
968
- ;
969
- const removed = listeners.delete(listenerId);
970
- if (removed) {
971
- logger.info("WillReconnect listener deregistered", { tunnelId, listenerId });
972
- if (listeners.size === 0) {
973
- this.tunnelWillReconnectListeners.delete(tunnelId);
974
- }
975
- } else {
976
- logger.warn("Attempted to deregister non-existent will-reconnect listener", { tunnelId, listenerId });
977
- }
978
- }
979
- deregisterReconnectingListener(tunnelId, listenerId) {
980
- const listeners = this.tunnelReconnectingListeners.get(tunnelId);
981
- if (!listeners) {
982
- logger.warn("No reconnecting listeners found for tunnel", { tunnelId });
983
- return;
984
- }
985
- ;
986
- const removed = listeners.delete(listenerId);
987
- if (removed) {
988
- logger.info("Reconnecting listener deregistered", { tunnelId, listenerId });
989
- if (listeners.size === 0) {
990
- this.tunnelReconnectingListeners.delete(tunnelId);
991
- }
992
- } else {
993
- logger.warn("Attempted to deregister non-existent reconnecting listener", { tunnelId, listenerId });
994
- }
995
- }
996
- deregisterReconnectionCompletedListener(tunnelId, listenerId) {
997
- const listeners = this.tunnelReconnectionCompletedListeners.get(tunnelId);
998
- if (!listeners) {
999
- logger.warn("No reconnection completed listeners found for tunnel", { tunnelId });
1000
- return;
1001
- }
1002
- const removed = listeners.delete(listenerId);
1003
- if (removed) {
1004
- logger.info("Reconnection completed listener deregistered", { tunnelId, listenerId });
1005
- if (listeners.size === 0) {
1006
- this.tunnelReconnectionCompletedListeners.delete(tunnelId);
1007
- }
1008
- } else {
1009
- logger.warn("Attempted to deregister non-existent reconnection completed listener", { tunnelId, listenerId });
1010
- }
1011
- }
1012
- deregisterReconnectionFailedListener(tunnelId, listenerId) {
1013
- const listeners = this.tunnelReconnectionFailedListeners.get(tunnelId);
1014
- if (!listeners) {
1015
- logger.warn("No reconnection failed listeners found for tunnel", { tunnelId });
1016
- return;
1017
- }
1018
- const removed = listeners.delete(listenerId);
1019
- if (removed) {
1020
- logger.info("Reconnection failed listener deregistered", { tunnelId, listenerId });
1021
- if (listeners.size === 0) {
1022
- this.tunnelReconnectionFailedListeners.delete(tunnelId);
1023
- }
1024
- } else {
1025
- logger.warn("Attempted to deregister non-existent reconnection failed listener", { tunnelId, listenerId });
1026
- }
1027
- }
1028
- async getLocalserverTlsInfo(tunnelId) {
1029
- const managed = this.tunnelsByTunnelId.get(tunnelId);
1030
- if (!managed) {
1031
- logger.error(`Tunnel "${tunnelId}" not found when fetching local server TLS info`);
1032
- return false;
1033
- }
1034
- try {
1035
- if (managed.isStopped) {
1036
- return false;
1037
- }
1038
- const tlsInfo = await managed.instance.getLocalServerTls();
1039
- if (tlsInfo) {
1040
- return tlsInfo;
1041
- }
1042
- return false;
1043
- } catch (e) {
1044
- logger.error(`Error fetching TLS info for tunnel "${tunnelId}": ${e instanceof Error ? e.message : e}`);
1045
- return false;
1046
- }
1047
- }
1048
- /**
1049
- * Sets up the stats callback for a tunnel during creation.
1050
- * This callback will update stored stats and notify all registered listeners.
1051
- */
1052
- setupStatsCallback(tunnelId, managed) {
1053
- try {
1054
- const callback = (usage) => {
1055
- this.updateStats(tunnelId, usage);
1056
- };
1057
- managed.instance.setUsageUpdateCallback(callback);
1058
- logger.debug("Stats callback set up for tunnel", { tunnelId });
1059
- } catch (error) {
1060
- logger.warn("Failed to set up stats callback", { tunnelId, error });
1061
- }
1062
- }
1063
- setupTunnelPollingErrorCallback(tunnelId, managed) {
1064
- try {
1065
- const callback = ({ error }) => {
1066
- try {
1067
- const errorMessage = error instanceof Error ? error.message : String(error);
1068
- logger.info("Tunnel reported polling error", { tunnelId, errorMessage });
1069
- const managedTunnel = this.tunnelsByTunnelId.get(tunnelId);
1070
- if (managedTunnel) {
1071
- managedTunnel.lastError = {
1072
- message: errorMessage,
1073
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1074
- isFatal: true
1075
- };
1076
- }
1077
- this.notifyPollingErrorListeners(tunnelId, errorMessage);
1078
- } catch (e) {
1079
- logger.warn("Error handling tunnel polling error callback", { tunnelId, e });
1080
- }
1081
- };
1082
- managed.instance.setPollingErrorCallback(callback);
1083
- logger.debug("Tunnel polling error callback set up for tunnel", { tunnelId });
1084
- } catch (error) {
1085
- logger.warn("Failed to set up tunnel polling error callback", { tunnelId, error });
1086
- }
1087
- }
1088
- notifyPollingErrorListeners(tunnelId, errorMsg) {
1089
- try {
1090
- const listeners = this.tunnelPollingErrorListeners.get(tunnelId);
1091
- if (!listeners) {
1092
- return;
1093
- }
1094
- for (const [id, listener] of listeners) {
1095
- try {
1096
- listener(tunnelId, errorMsg);
1097
- } catch (err) {
1098
- logger.debug("Error in polling-error-listener callback", { listenerId: id, tunnelId, err });
1099
- }
1100
- }
1101
- } catch (err) {
1102
- logger.debug("Failed to notify polling error listeners", { tunnelId, err });
1103
- }
1104
- }
1105
- notifyErrorListeners(tunnelId, errorMsg, isFatal) {
1106
- try {
1107
- const listeners = this.tunnelErrorListeners.get(tunnelId);
1108
- if (!listeners) {
1109
- return;
1110
- }
1111
- for (const [id, listener] of listeners) {
1112
- try {
1113
- listener(tunnelId, errorMsg, isFatal);
1114
- } catch (err) {
1115
- logger.debug("Error in error-listener callback", { listenerId: id, tunnelId, err });
1116
- }
1117
- }
1118
- } catch (err) {
1119
- logger.debug("Failed to notify error listeners", { tunnelId, err });
1120
- }
1121
- }
1122
- setupErrorCallback(tunnelId, managed) {
1123
- try {
1124
- const callback = ({ errorNo, error, recoverable }) => {
1125
- try {
1126
- const msg = typeof error === "string" ? error : String(error);
1127
- const isFatal = true;
1128
- logger.debug("Tunnel reported error", { tunnelId, errorNo, errorMsg: msg, recoverable });
1129
- const managedTunnel = this.tunnelsByTunnelId.get(tunnelId);
1130
- if (managedTunnel) {
1131
- managedTunnel.lastError = {
1132
- message: msg,
1133
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1134
- isFatal: false
1135
- };
1136
- }
1137
- this.notifyErrorListeners(tunnelId, msg, isFatal);
1138
- } catch (e) {
1139
- logger.warn("Error handling tunnel error callback", { tunnelId, e });
1140
- }
1141
- };
1142
- managed.instance.setTunnelErrorCallback(callback);
1143
- logger.debug("Error callback set up for tunnel", { tunnelId });
1144
- } catch (error) {
1145
- logger.warn("Failed to set up error callback", { tunnelId, error });
1146
- }
1147
- }
1148
- setupDisconnectCallback(tunnelId, managed) {
1149
- try {
1150
- const callback = ({ error, messages }) => {
1151
- try {
1152
- logger.debug("Tunnel disconnected", { tunnelId, error, messages });
1153
- const managedTunnel = this.tunnelsByTunnelId.get(tunnelId);
1154
- if (managedTunnel) {
1155
- managedTunnel.isStopped = true;
1156
- managedTunnel.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
1157
- }
1158
- const listeners = this.tunnelDisconnectListeners.get(tunnelId);
1159
- if (!listeners) {
1160
- return;
1161
- }
1162
- for (const [id, listener] of listeners) {
1163
- try {
1164
- listener(tunnelId, error, messages);
1165
- } catch (err) {
1166
- logger.debug("Error in disconnect-listener callback", { listenerId: id, tunnelId, err });
1167
- }
1168
- }
1169
- } catch (e) {
1170
- logger.warn("Error handling tunnel disconnect callback", { tunnelId, e });
1171
- }
1172
- };
1173
- managed.instance.setTunnelDisconnectedCallback(callback);
1174
- logger.debug("Disconnect callback set up for tunnel", { tunnelId });
1175
- } catch (error) {
1176
- logger.warn("Failed to set up disconnect callback", { tunnelId, error });
1177
- }
1178
- }
1179
- /**
1180
- * Called when the tunnel disconnects and the SDK is about to start reconnecting.
1181
- * Notifies registered will-reconnect listeners.
1182
- */
1183
- setupWillReconnectCallback(tunnelId, managed) {
1184
- try {
1185
- const callback = ({ error, messages }) => {
1186
- try {
1187
- logger.info("Tunnel will reconnect", { tunnelId, error, messages });
1188
- const listeners = this.tunnelWillReconnectListeners.get(tunnelId);
1189
- if (!listeners) {
1190
- return;
1191
- }
1192
- for (const [id, listener] of listeners) {
1193
- try {
1194
- listener(tunnelId, error, messages);
1195
- } catch (err) {
1196
- logger.debug("Error in will-reconnect-listener callback", { listenerId: id, tunnelId, err });
1197
- }
1198
- }
1199
- } catch (e) {
1200
- logger.warn("Error handling will-reconnect callback", { tunnelId, e });
1201
- }
1202
- };
1203
- managed.instance.setWillReconnectCallback(callback);
1204
- logger.debug("WillReconnect callback set up for tunnel", { tunnelId });
1205
- } catch (error) {
1206
- logger.warn("Failed to set up will-reconnect callback", { tunnelId, error });
1207
- }
1208
- }
1209
- /**
1210
- * Called for each reconnection attempt with the current retry count.
1211
- * Notifies registered reconnecting listeners.
1212
- */
1213
- setupReconnectingCallback(tunnelId, managed) {
1214
- try {
1215
- const callback = ({ retryCnt }) => {
1216
- try {
1217
- logger.info("Tunnel reconnecting", { tunnelId, retryCnt });
1218
- const listeners = this.tunnelReconnectingListeners.get(tunnelId);
1219
- if (!listeners) {
1220
- return;
1221
- }
1222
- for (const [id, listener] of listeners) {
1223
- try {
1224
- listener(tunnelId, retryCnt);
1225
- } catch (err) {
1226
- logger.debug("Error in reconnecting-listener callback", { listenerId: id, tunnelId, err });
1227
- }
1228
- }
1229
- } catch (e) {
1230
- logger.warn("Error handling reconnecting callback", { tunnelId, e });
1231
- }
1232
- };
1233
- managed.instance.setReconnectingCallback(callback);
1234
- logger.debug("Reconnecting callback set up for tunnel", { tunnelId });
1235
- } catch (error) {
1236
- logger.warn("Failed to set up reconnecting callback", { tunnelId, error });
1237
- }
1238
- }
1239
- /**
1240
- * Called when reconnection succeeds. Updates tunnel state back to active,
1241
- * and notifies registered reconnection-completed and start listeners with new URLs.
1242
- */
1243
- setupReconnectionCompletedCallback(tunnelId, managed) {
1244
- try {
1245
- const callback = ({ urls }) => {
1246
- try {
1247
- logger.info("Tunnel reconnection completed", { tunnelId, urls });
1248
- const managedTunnel = this.tunnelsByTunnelId.get(tunnelId);
1249
- if (managedTunnel) {
1250
- managedTunnel.isStopped = false;
1251
- managedTunnel.startedAt = (/* @__PURE__ */ new Date()).toISOString();
1252
- managedTunnel.stoppedAt = null;
1253
- }
1254
- const listeners = this.tunnelReconnectionCompletedListeners.get(tunnelId);
1255
- if (listeners) {
1256
- for (const [id, listener] of listeners) {
1257
- try {
1258
- listener(tunnelId, urls);
1259
- } catch (err) {
1260
- logger.debug("Error in reconnection-completed-listener callback", { listenerId: id, tunnelId, err });
1261
- }
1262
- }
1263
- }
1264
- const startListeners = this.tunnelStartListeners.get(tunnelId);
1265
- if (startListeners) {
1266
- for (const [id, listener] of startListeners) {
1267
- try {
1268
- listener(tunnelId, urls);
1269
- } catch (err) {
1270
- logger.debug("Error in start-listener callback on reconnection", { listenerId: id, tunnelId, err });
1271
- }
1272
- }
1273
- }
1274
- } catch (e) {
1275
- logger.warn("Error handling reconnection-completed callback", { tunnelId, e });
1276
- }
1277
- };
1278
- managed.instance.setReconnectionCompletedCallback(callback);
1279
- logger.debug("ReconnectionCompleted callback set up for tunnel", { tunnelId });
1280
- } catch (error) {
1281
- logger.warn("Failed to set up reconnection-completed callback", { tunnelId, error });
1282
- }
1283
- }
1284
- /**
1285
- * Called when all reconnection attempts are exhausted.
1286
- * Marks the tunnel as stopped and notifies registered reconnection-failed listeners.
1287
- */
1288
- setupReconnectionFailedCallback(tunnelId, managed) {
1289
- try {
1290
- const callback = ({ retryCnt }) => {
1291
- try {
1292
- logger.error("Tunnel reconnection failed", { tunnelId, retryCnt });
1293
- const managedTunnel = this.tunnelsByTunnelId.get(tunnelId);
1294
- if (managedTunnel) {
1295
- managedTunnel.isStopped = true;
1296
- managedTunnel.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
1297
- }
1298
- const listeners = this.tunnelReconnectionFailedListeners.get(tunnelId);
1299
- if (!listeners) {
1300
- return;
1301
- }
1302
- for (const [id, listener] of listeners) {
1303
- try {
1304
- listener(tunnelId, retryCnt);
1305
- } catch (err) {
1306
- logger.debug("Error in reconnection-failed-listener callback", { listenerId: id, tunnelId, err });
1307
- }
1308
- }
1309
- } catch (e) {
1310
- logger.warn("Error handling reconnection-failed callback", { tunnelId, e });
1311
- }
1312
- };
1313
- managed.instance.setReconnectionFailedCallback(callback);
1314
- logger.debug("ReconnectionFailed callback set up for tunnel", { tunnelId });
1315
- } catch (error) {
1316
- logger.warn("Failed to set up reconnection-failed callback", { tunnelId, error });
1317
- }
1318
- }
1319
- setUpTunnelWorkerErrorCallback(tunnelId, managed) {
1320
- try {
1321
- const callback = (error) => {
1322
- try {
1323
- logger.debug("Error in Tunnel Worker", { tunnelId, errorMessage: error.message });
1324
- const listeners = this.tunnelWorkerErrorListeners.get(tunnelId);
1325
- if (!listeners) {
1326
- return;
1327
- }
1328
- for (const [id, listener] of listeners) {
1329
- try {
1330
- listener(tunnelId, error);
1331
- } catch (err) {
1332
- logger.debug("Error in worker-error-listener callback", { listenerId: id, tunnelId, err });
1333
- }
1334
- }
1335
- } catch (e) {
1336
- logger.warn("Error handling tunnel worker error callback", { tunnelId, e });
1337
- }
1338
- };
1339
- managed.instance.setWorkerErrorCallback(callback);
1340
- logger.debug("Disconnect callback set up for tunnel", { tunnelId });
1341
- } catch (error) {
1342
- logger.warn("Failed to setup tunnel worker error callback");
1343
- }
1344
- }
1345
- /**
1346
- * Updates the stored stats for a tunnel and notifies all registered listeners.
1347
- */
1348
- updateStats(tunnelId, rawUsage) {
1349
- try {
1350
- const normalizedStats = this.normalizeStats(rawUsage);
1351
- const existingStats = this.tunnelStats.get(tunnelId) || [];
1352
- const updatedStats = [...existingStats, normalizedStats];
1353
- this.tunnelStats.set(tunnelId, updatedStats);
1354
- const tunnelListeners = this.tunnelStatsListeners.get(tunnelId);
1355
- if (tunnelListeners) {
1356
- for (const [listenerId, listener] of tunnelListeners) {
1357
- try {
1358
- listener(tunnelId, normalizedStats);
1359
- } catch (error) {
1360
- logger.warn("Error in stats listener callback", { listenerId, tunnelId, error });
1361
- }
1362
- }
1363
- }
1364
- logger.debug("Stats updated and listeners notified", {
1365
- tunnelId,
1366
- listenersCount: tunnelListeners?.size || 0
1367
- });
1368
- } catch (error) {
1369
- logger.warn("Error updating stats", { tunnelId, error });
1370
- }
1371
- }
1372
- /**
1373
- * Normalizes raw usage data from the SDK into a consistent TunnelStats format.
1374
- */
1375
- normalizeStats(rawStats) {
1376
- const elapsed = this.parseNumber(rawStats.elapsedTime ?? 0);
1377
- const liveConns = this.parseNumber(rawStats.numLiveConnections ?? 0);
1378
- const totalConns = this.parseNumber(rawStats.numTotalConnections ?? 0);
1379
- const reqBytes = this.parseNumber(rawStats.numTotalReqBytes ?? 0);
1380
- const resBytes = this.parseNumber(rawStats.numTotalResBytes ?? 0);
1381
- const txBytes = this.parseNumber(rawStats.numTotalTxBytes ?? 0);
1382
- return {
1383
- elapsedTime: elapsed,
1384
- numLiveConnections: liveConns,
1385
- numTotalConnections: totalConns,
1386
- numTotalReqBytes: reqBytes,
1387
- numTotalResBytes: resBytes,
1388
- numTotalTxBytes: txBytes
1389
- };
1390
- }
1391
- parseNumber(value) {
1392
- const parsed = typeof value === "number" ? value : parseInt(String(value), 10);
1393
- return isNaN(parsed) ? 0 : parsed;
1394
- }
1395
- /**
1396
- * Read serve path only from config.optional.serve.
1397
- */
1398
- resolveServePath(config) {
1399
- const optional = config.optional;
1400
- const servePath = optional?.serve;
1401
- logger.debug("resolveServePath", { servePath, hasOptional: !!optional, optionalKeys: optional ? Object.keys(optional) : [] });
1402
- return servePath;
1403
- }
1404
- startStaticFileServer(managed) {
1405
- try {
1406
- const __filename4 = fileURLToPath2(import.meta.url);
1407
- const __dirname4 = path.dirname(__filename4);
1408
- const fileServerWorkerPath = path.join(__dirname4, "workers", "file_serve_worker.cjs");
1409
- logger.info("Starting static file server worker", {
1410
- dir: managed.serve,
1411
- forwarding: JSON.stringify(managed.tunnelConfig?.forwarding),
1412
- workerPath: fileServerWorkerPath
1413
- });
1414
- const staticServerWorker = new Worker(fileServerWorkerPath, {
1415
- workerData: {
1416
- dir: managed.serve,
1417
- forwarding: managed.tunnelConfig?.forwarding
1418
- }
1419
- });
1420
- staticServerWorker.on("message", (msg) => {
1421
- switch (msg.type) {
1422
- case "started":
1423
- logger.info("Static file server started", { dir: managed.serve, port: msg.portNum });
1424
- break;
1425
- case "warning":
1426
- if (msg.code === "INVALID_TUNNEL_SERVE_PATH") {
1427
- managed.warnings = managed.warnings ?? [];
1428
- managed.warnings.push({ code: msg.code, message: msg.message });
1429
- }
1430
- printer_default.warn(msg.message);
1431
- break;
1432
- case "error":
1433
- managed.warnings = managed.warnings ?? [];
1434
- managed.warnings.push({
1435
- code: "UNKNOWN_WARNING",
1436
- message: msg.message
1437
- });
1438
- break;
1439
- }
1440
- });
1441
- managed.serveWorker = staticServerWorker;
1442
- } catch (error) {
1443
- logger.error("Error starting static file server", error);
1444
- }
1445
- }
1446
- };
1447
-
1448
- // src/types.ts
1449
- var TunnelStateType = /* @__PURE__ */ ((TunnelStateType2) => {
1450
- TunnelStateType2["New"] = "idle";
1451
- TunnelStateType2["Starting"] = "starting";
1452
- TunnelStateType2["Running"] = "running";
1453
- TunnelStateType2["Live"] = "live";
1454
- TunnelStateType2["Closed"] = "closed";
1455
- TunnelStateType2["Exited"] = "exited";
1456
- return TunnelStateType2;
1457
- })(TunnelStateType || {});
1458
- var TunnelErrorCodeType = /* @__PURE__ */ ((TunnelErrorCodeType2) => {
1459
- TunnelErrorCodeType2["NonResponsive"] = "non_responsive";
1460
- TunnelErrorCodeType2["FailedToConnect"] = "failed_to_connect";
1461
- TunnelErrorCodeType2["ErrorInAdditionalForwarding"] = "additional_forwarding_error";
1462
- TunnelErrorCodeType2["WebdebuggerError"] = "webdebugger_error";
1463
- TunnelErrorCodeType2["NoError"] = "";
1464
- return TunnelErrorCodeType2;
1465
- })(TunnelErrorCodeType || {});
1466
- var TunnelWarningCode = /* @__PURE__ */ ((TunnelWarningCode2) => {
1467
- TunnelWarningCode2["InvalidTunnelServePath"] = "INVALID_TUNNEL_SERVE_PATH";
1468
- TunnelWarningCode2["UnknownWarning"] = "UNKNOWN_WARNING";
1469
- return TunnelWarningCode2;
1470
- })(TunnelWarningCode || {});
1471
- var ErrorCode = {
1472
- InvalidRequestMethodError: "INVALID_REQUEST_METHOD",
1473
- InvalidRequestBodyError: "COULD_NOT_READ_BODY",
1474
- InternalServerError: "INTERNAL_SERVER_ERROR",
1475
- InvalidBodyFormatError: "INVALID_DATA_FORMAT",
1476
- ErrorStartingTunnel: "ERROR_STARTING_TUNNEL",
1477
- TunnelNotFound: "TUNNEL_WITH_ID_OR_CONFIG_ID_NOT_FOUND",
1478
- TunnelAlreadyRunningError: "TUNNEL_WITH_ID_OR_CONFIG_ID_ALREADY_RUNNING",
1479
- WebsocketUpgradeFailError: "WEBSOCKET_UPGRADE_FAILED",
1480
- RemoteManagementAlreadyRunning: "REMOTE_MANAGEMENT_ALREADY_RUNNING",
1481
- RemoteManagementNotRunning: "REMOTE_MANAGEMENT_NOT_RUNNING",
1482
- RemoteManagementDeserializationFailed: "REMOTE_MANAGEMENT_DESERIALIZATION_FAILED"
1483
- };
1484
- function isErrorResponse(obj) {
1485
- return typeof obj === "object" && obj !== null && "code" in obj && "message" in obj && typeof obj.message === "string" && Object.values(ErrorCode).includes(obj.code);
1486
- }
1487
- function newErrorResponse(codeOrError, message) {
1488
- if (typeof codeOrError === "object") {
1489
- return codeOrError;
1490
- }
1491
- return {
1492
- code: codeOrError,
1493
- message
1494
- };
1495
- }
1496
- function NewResponseObject(data) {
1497
- const encoder = new TextEncoder();
1498
- const bytes = encoder.encode(JSON.stringify(data));
1499
- return {
1500
- response: bytes,
1501
- requestid: "",
1502
- command: "",
1503
- error: false,
1504
- errorresponse: {}
1505
- };
1506
- }
1507
- function NewErrorResponseObject(errorResponse) {
1508
- return {
1509
- response: new Uint8Array(),
1510
- requestid: "",
1511
- command: "",
1512
- error: true,
1513
- errorresponse: errorResponse
1514
- };
1515
- }
1516
- function newStatus(tunnelState, errorCode, errorMsg) {
1517
- let assignedState = tunnelState;
1518
- if (tunnelState === "live" /* Live */) {
1519
- assignedState = "running" /* Running */;
1520
- } else if (tunnelState === "idle" /* New */) {
1521
- assignedState = "idle" /* New */;
1522
- } else if (tunnelState === "closed" /* Closed */) {
1523
- assignedState = "exited" /* Exited */;
1524
- }
1525
- const now = (/* @__PURE__ */ new Date()).toISOString();
1526
- return {
1527
- state: assignedState,
1528
- errorcode: errorCode,
1529
- errormsg: errorMsg,
1530
- createdtimestamp: now,
1531
- starttimestamp: now,
1532
- endtimestamp: now,
1533
- warnings: []
1534
- };
1535
- }
1536
- function newStats() {
1537
- return {
1538
- numLiveConnections: 0,
1539
- numTotalConnections: 0,
1540
- numTotalReqBytes: 0,
1541
- numTotalResBytes: 0,
1542
- numTotalTxBytes: 0,
1543
- elapsedTime: 0
1544
- };
1545
- }
1546
- var RemoteManagementStatus = {
1547
- Connecting: "CONNECTING",
1548
- Disconnecting: "DISCONNECTING",
1549
- Reconnecting: "RECONNECTING",
1550
- Running: "RUNNING",
1551
- NotRunning: "NOT_RUNNING",
1552
- Error: "ERROR"
1553
- };
1554
-
1555
- // src/remote_management/remote_schema.ts
1556
- import { TunnelType } from "@pinggy/pinggy";
1557
- import { z } from "zod";
1558
- var HeaderModificationSchema = z.object({
1559
- key: z.string(),
1560
- value: z.array(z.string()).nullable().optional(),
1561
- type: z.enum(["add", "remove", "update"])
1562
- });
1563
- var AdditionalForwardingSchema = z.object({
1564
- remoteDomain: z.string().optional(),
1565
- remotePort: z.number().optional(),
1566
- localDomain: z.string(),
1567
- localPort: z.number()
1568
- });
1569
- var TunnelConfigSchema = z.object({
1570
- allowPreflight: z.boolean().optional(),
1571
- // primary key
1572
- allowpreflight: z.boolean().optional(),
1573
- // legacy key
1574
- autoreconnect: z.boolean(),
1575
- basicauth: z.array(z.object({ username: z.string(), password: z.string() })).nullable(),
1576
- bearerauth: z.array(z.string()).nullable(),
1577
- configid: z.string(),
1578
- configname: z.string(),
1579
- greetmsg: z.string().optional(),
1580
- force: z.boolean(),
1581
- forwardedhost: z.string(),
1582
- fullRequestUrl: z.boolean(),
1583
- headermodification: z.array(HeaderModificationSchema),
1584
- httpsOnly: z.boolean(),
1585
- internalwebdebuggerport: z.number(),
1586
- ipwhitelist: z.array(z.string()).nullable(),
1587
- localport: z.number(),
1588
- localsservertls: z.union([z.boolean(), z.string()]),
1589
- localservertlssni: z.string().nullable(),
1590
- regioncode: z.string(),
1591
- noReverseProxy: z.boolean(),
1592
- serveraddress: z.string(),
1593
- serverport: z.number(),
1594
- statusCheckInterval: z.number(),
1595
- token: z.string(),
1596
- tunnelTimeout: z.number(),
1597
- type: z.enum([
1598
- TunnelType.Http,
1599
- TunnelType.Tcp,
1600
- TunnelType.Udp,
1601
- TunnelType.Tls,
1602
- TunnelType.TlsTcp
1603
- ]),
1604
- webdebuggerport: z.number(),
1605
- xff: z.string(),
1606
- additionalForwarding: z.array(AdditionalForwardingSchema).optional(),
1607
- serve: z.string().optional()
1608
- }).superRefine((data, ctx) => {
1609
- if (data.allowPreflight === void 0 && data.allowpreflight === void 0) {
1610
- ctx.addIssue({
1611
- code: "custom",
1612
- message: "Either allowPreflight or allowpreflight is required",
1613
- path: ["allowPreflight"]
1614
- });
1615
- }
1616
- }).transform((data) => ({
1617
- ...data,
1618
- allowPreflight: data.allowPreflight ?? data.allowpreflight,
1619
- allowpreflight: data.allowPreflight ?? data.allowpreflight
1620
- }));
1621
- var StartSchema = z.object({
1622
- tunnelID: z.string().nullable().optional(),
1623
- tunnelConfig: TunnelConfigSchema
1624
- });
1625
- var StopSchema = z.object({
1626
- tunnelID: z.string().min(1)
1627
- });
1628
- var GetSchema = StopSchema;
1629
- var RestartSchema = StopSchema;
1630
- var UpdateConfigSchema = z.object({
1631
- tunnelConfig: TunnelConfigSchema
1632
- });
1633
- var ForwardingEntryV2Schema = z.object({
1634
- listenAddress: z.string().optional(),
1635
- address: z.string(),
1636
- type: z.enum([TunnelType.Http, TunnelType.Tcp, TunnelType.Udp, TunnelType.Tls, TunnelType.TlsTcp]).optional()
1637
- });
1638
- var TunnelConfigV1Schema = z.object({
1639
- // Meta Info
1640
- version: z.string(),
1641
- name: z.string(),
1642
- configId: z.string(),
1643
- // General tunnel configurations
1644
- serverAddress: z.string().optional(),
1645
- token: z.string().optional(),
1646
- autoReconnect: z.boolean().optional(),
1647
- reconnectInterval: z.number().optional(),
1648
- maxReconnectAttempts: z.number().optional(),
1649
- force: z.boolean(),
1650
- keepAliveInterval: z.number().optional(),
1651
- webDebugger: z.string(),
1652
- //Forwarding
1653
- // Either a URL string (e.g. "https://localhost:5555") or an array of forwarding entries.
1654
- forwarding: z.union([
1655
- z.string(),
1656
- z.array(ForwardingEntryV2Schema)
1657
- ]),
1658
- // IP whitelist
1659
- ipWhitelist: z.array(z.string()).optional(),
1660
- basicAuth: z.array(z.object({ username: z.string(), password: z.string() })).optional(),
1661
- bearerTokenAuth: z.array(z.string()).optional(),
1662
- headerModification: z.array(HeaderModificationSchema).optional(),
1663
- reverseProxy: z.boolean().optional(),
1664
- xForwardedFor: z.boolean().optional(),
1665
- httpsOnly: z.boolean().optional(),
1666
- originalRequestUrl: z.boolean().optional(),
1667
- allowPreflight: z.boolean().optional(),
1668
- serve: z.string().optional(),
1669
- optional: z.record(z.string(), z.unknown()).optional()
1670
- });
1671
- var StartV2Schema = z.object({
1672
- tunnelID: z.string().nullable().optional(),
1673
- tunnelConfig: TunnelConfigV1Schema
1674
- });
1675
- var UpdateConfigV2Schema = z.object({
1676
- tunnelConfig: TunnelConfigV1Schema
1677
- });
1678
- function pinggyOptionsToTunnelConfigV1(opts, configStoredInCli) {
1679
- const parsedTokens = opts.bearerTokenAuth ? Array.isArray(opts.bearerTokenAuth) ? opts.bearerTokenAuth : JSON.parse(opts.bearerTokenAuth) : [];
1680
- return {
1681
- version: configStoredInCli.version || "1.0",
1682
- name: configStoredInCli.name || "",
1683
- configId: configStoredInCli.configId || "",
1684
- serverAddress: opts.serverAddress || "a.pinggy.io:443",
1685
- token: opts.token || "",
1686
- autoReconnect: opts.autoReconnect ?? true,
1687
- force: opts.force ?? false,
1688
- webDebugger: opts.webDebugger || "",
1689
- forwarding: opts.forwarding ? opts.forwarding : "",
1690
- ipWhitelist: opts.ipWhitelist ? Array.isArray(opts.ipWhitelist) ? opts.ipWhitelist : JSON.parse(opts.ipWhitelist) : [],
1691
- basicAuth: opts.basicAuth && Object.keys(opts.basicAuth).length ? opts.basicAuth : void 0,
1692
- bearerTokenAuth: parsedTokens.length ? parsedTokens : void 0,
1693
- headerModification: opts.headerModification || [],
1694
- reverseProxy: opts.reverseProxy ?? false,
1695
- xForwardedFor: !!opts.xForwardedFor,
1696
- httpsOnly: opts.httpsOnly ?? false,
1697
- originalRequestUrl: opts.originalRequestUrl ?? false,
1698
- allowPreflight: opts.allowPreflight ?? false,
1699
- optional: opts.optional || {}
1700
- };
1701
- }
1702
- function tunnelConfigToPinggyOptions(config) {
1703
- const forwardingData = [];
1704
- forwardingData.push({
1705
- address: `${config.forwardedhost}:${config.localport}`,
1706
- type: config.type || TunnelType.Http
1707
- // Default to HTTP for the primary forwarding entry
1708
- });
1709
- if (config.additionalForwarding && Array.isArray(config.additionalForwarding)) {
1710
- config.additionalForwarding.forEach((entry) => {
1711
- if (entry.localDomain && entry.localPort && entry.remoteDomain) {
1712
- const listenAddress = entry.remotePort && isValidPort(entry.remotePort) ? `${entry.remoteDomain}:${entry.remotePort}` : entry.remoteDomain;
1713
- forwardingData.push({
1714
- address: `${entry.localDomain}:${entry.localPort}`,
1715
- listenAddress,
1716
- type: TunnelType.Http
1717
- });
1718
- }
1719
- });
1720
- }
1721
- return {
1722
- token: config.token || "",
1723
- serverAddress: config.serveraddress || "free.pinggy.io",
1724
- forwarding: forwardingData,
1725
- webDebugger: config.webdebuggerport ? `localhost:${config.webdebuggerport}` : "",
1726
- ipWhitelist: config.ipwhitelist || [],
1727
- basicAuth: config.basicauth ? config.basicauth : [],
1728
- bearerTokenAuth: config.bearerauth || [],
1729
- headerModification: config.headermodification,
1730
- xForwardedFor: !!config.xff,
1731
- httpsOnly: config.httpsOnly,
1732
- originalRequestUrl: config.fullRequestUrl,
1733
- allowPreflight: config.allowPreflight,
1734
- reverseProxy: config.noReverseProxy,
1735
- force: config.force,
1736
- autoReconnect: config.autoreconnect,
1737
- optional: {
1738
- sniServerName: config.localservertlssni || ""
1739
- }
1740
- };
1741
- }
1742
- function pinggyOptionsToTunnelConfig(opts, configid, configName, localserverTls, greetMsg, serve) {
1743
- let primaryEntry;
1744
- let additionalEntries = [];
1745
- if (Array.isArray(opts.forwarding)) {
1746
- primaryEntry = opts.forwarding.find((e) => !e.listenAddress) ?? opts.forwarding[0];
1747
- additionalEntries = opts.forwarding.filter(
1748
- (e) => e !== primaryEntry && Boolean(e.listenAddress)
1749
- );
1750
- }
1751
- const forwarding = primaryEntry ? String(primaryEntry.address) : String(opts.forwarding);
1752
- const [parsedForwardedHost, portStr] = forwarding.split(":");
1753
- const parsedLocalPort = parseInt(portStr, 10);
1754
- const tunnelType = primaryEntry?.type ?? TunnelType.Http;
1755
- const additionalForwarding = additionalEntries.map((e) => {
1756
- const [localDomain, localPortStr] = String(e.address).split(":");
1757
- const [remoteDomain, remotePortStr] = String(e.listenAddress).split(":");
1758
- const localPort = parseInt(localPortStr, 10);
1759
- const remotePort = parseInt(remotePortStr, 10);
1760
- return {
1761
- localDomain,
1762
- localPort: isNaN(localPort) ? 0 : localPort,
1763
- remoteDomain,
1764
- remotePort: isNaN(remotePort) ? 0 : remotePort
1765
- };
1766
- });
1767
- const parsedTokens = opts.bearerTokenAuth ? Array.isArray(opts.bearerTokenAuth) ? opts.bearerTokenAuth : JSON.parse(opts.bearerTokenAuth) : [];
1768
- return {
1769
- allowPreflight: opts.allowPreflight ?? false,
1770
- allowpreflight: opts.allowPreflight ?? false,
1771
- autoreconnect: opts.autoReconnect ?? false,
1772
- basicauth: opts.basicAuth && Object.keys(opts.basicAuth).length ? opts.basicAuth : null,
1773
- bearerauth: parsedTokens.length ? [parsedTokens.join(",")] : null,
1774
- configid,
1775
- configname: configName,
1776
- greetmsg: greetMsg || "",
1777
- force: opts.force ?? false,
1778
- forwardedhost: parsedForwardedHost || "localhost",
1779
- fullRequestUrl: opts.originalRequestUrl ?? false,
1780
- headermodification: opts.headerModification || [],
1781
- //structured list
1782
- httpsOnly: opts.httpsOnly ?? false,
1783
- internalwebdebuggerport: 0,
1784
- ipwhitelist: opts.ipWhitelist ? Array.isArray(opts.ipWhitelist) ? opts.ipWhitelist : JSON.parse(opts.ipWhitelist) : null,
1785
- localport: parsedLocalPort || 0,
1786
- localservertlssni: null,
1787
- regioncode: "",
1788
- noReverseProxy: opts.reverseProxy ?? false,
1789
- serveraddress: opts.serverAddress || "free.pinggy.io",
1790
- serverport: 0,
1791
- statusCheckInterval: 0,
1792
- token: opts.token || "",
1793
- tunnelTimeout: 0,
1794
- type: tunnelType,
1795
- webdebuggerport: Number(opts.webDebugger?.split(":")[0]) || 0,
1796
- xff: opts.xForwardedFor ? "1" : "",
1797
- localsservertls: localserverTls || false,
1798
- additionalForwarding: additionalForwarding || [],
1799
- serve: serve || ""
1800
- };
1801
- }
1802
-
1803
- // src/remote_management/handler.ts
1804
- var TunnelOperations = class {
1805
- constructor() {
1806
- this.tunnelManager = TunnelManager.getInstance();
1807
- }
1808
- buildStatus(tunnelId, state, errorCode) {
1809
- const status = newStatus(state, errorCode, "");
1810
- try {
1811
- const managed = this.tunnelManager.getManagedTunnel("", tunnelId);
1812
- if (managed) {
1813
- status.createdtimestamp = managed.createdAt || "";
1814
- status.starttimestamp = managed.startedAt || "";
1815
- status.endtimestamp = managed.stoppedAt || "";
1816
- }
1817
- if (managed?.lastError) {
1818
- status.lastError = managed.lastError;
1819
- }
1820
- } catch (e) {
1821
- }
1822
- return status;
1823
- }
1824
- // --- Placeholder response ---
1825
- buildPendingTunnelResponse(tunnelid, tunnelConfig, configid, tunnelName, serve) {
1826
- return {
1827
- tunnelid,
1828
- remoteurls: [],
1829
- tunnelconfig: pinggyOptionsToTunnelConfig(tunnelConfig, configid, tunnelName, false, void 0, serve),
1830
- status: this.buildStatus(tunnelid, "starting" /* Starting */, "" /* NoError */),
1831
- stats: newStats()
1832
- };
1833
- }
1834
- buildPendingTunnelResponseV2(tunnelid, tunnelConfig, configFromCli, configid, tunnelName, serve) {
1835
- return {
1836
- tunnelid,
1837
- remoteurls: [],
1838
- tunnelconfig: pinggyOptionsToTunnelConfigV1(tunnelConfig, configFromCli),
1839
- status: this.buildStatus(tunnelid, "starting" /* Starting */, "" /* NoError */),
1840
- stats: newStats(),
1841
- greetmsg: ""
1842
- };
1843
- }
1844
- // --- Helper to construct TunnelResponse ---
1845
- async buildTunnelResponse(tunnelid, tunnelConfig, configid, tunnelName, serve) {
1846
- const [status, stats, tlsInfo, greetMsg, remoteurls] = await Promise.all([
1847
- this.tunnelManager.getTunnelStatus(tunnelid),
1848
- this.tunnelManager.getLatestTunnelStats(tunnelid) || newStats(),
1849
- this.tunnelManager.getLocalserverTlsInfo(tunnelid),
1850
- this.tunnelManager.getTunnelGreetMessage(tunnelid),
1851
- this.tunnelManager.getTunnelUrls(tunnelid)
1852
- ]);
1853
- return {
1854
- tunnelid,
1855
- remoteurls,
1856
- tunnelconfig: pinggyOptionsToTunnelConfig(tunnelConfig, configid, tunnelName, tlsInfo, greetMsg),
1857
- status: this.buildStatus(tunnelid, status, "" /* NoError */),
1858
- stats
1859
- };
1860
- }
1861
- async buildTunnelResponseV2(tunnelid, tunnelConfig, configFromCli, configid, tunnelName, serve) {
1862
- const [status, stats, greetMsg, remoteurls] = await Promise.all([
1863
- this.tunnelManager.getTunnelStatus(tunnelid),
1864
- this.tunnelManager.getLatestTunnelStats(tunnelid) || newStats(),
1865
- this.tunnelManager.getTunnelGreetMessage(tunnelid),
1866
- this.tunnelManager.getTunnelUrls(tunnelid)
1867
- ]);
1868
- return {
1869
- tunnelid,
1870
- remoteurls,
1871
- tunnelconfig: pinggyOptionsToTunnelConfigV1(tunnelConfig, configFromCli),
1872
- status: this.buildStatus(tunnelid, status, "" /* NoError */),
1873
- stats,
1874
- greetmsg: greetMsg
1875
- };
1876
- }
1877
- error(code, err, fallback) {
1878
- return newErrorResponse({
1879
- code,
1880
- message: err instanceof Error ? err.message : fallback
1881
- });
1882
- }
1883
- // --- Operations ---
1884
- async handleStart(config, noWait = false) {
1885
- try {
1886
- const opts = tunnelConfigToPinggyOptions(config);
1887
- const managed = await this.tunnelManager.createTunnel({
1888
- ...opts,
1889
- configId: config.configid,
1890
- name: config.configname,
1891
- optional: {
1892
- serve: config.serve
1893
- }
1894
- });
1895
- const { tunnelid, tunnelName, serve, tunnelConfig } = managed;
1896
- const startPromise = this.tunnelManager.startTunnel(tunnelid);
1897
- if (noWait) {
1898
- startPromise.catch((err) => {
1899
- logger.error("No-wait startTunnel failed", { tunnelid, err: String(err) });
1900
- });
1901
- return this.buildPendingTunnelResponse(tunnelid, tunnelConfig, config.configid, tunnelName, serve);
1902
- }
1903
- await startPromise;
1904
- const tunnelPconfig = await this.tunnelManager.getTunnelConfig("", tunnelid);
1905
- return this.buildTunnelResponse(tunnelid, tunnelPconfig, config.configid, tunnelName, serve);
1906
- } catch (err) {
1907
- return this.error(ErrorCode.ErrorStartingTunnel, err, "Unknown error occurred while starting tunnel");
1908
- }
1909
- }
1910
- async handleStartV2(config, noWait = false) {
1911
- try {
1912
- const managed = await this.tunnelManager.createTunnel(config);
1913
- const { tunnelid, serve, tunnelConfig } = managed;
1914
- await this.tunnelManager.startTunnel(tunnelid);
1915
- const tunnelPconfig = await this.tunnelManager.getTunnelConfig("", tunnelid);
1916
- return this.buildTunnelResponseV2(tunnelid, tunnelPconfig, config, config.configId, config.name, config.serve);
1917
- } catch (err) {
1918
- return this.error(ErrorCode.ErrorStartingTunnel, err, "Unknown error occurred while starting tunnel");
1919
- }
1920
- }
1921
- async handleUpdateConfig(config, noWait = false) {
1922
- try {
1923
- const opts = tunnelConfigToPinggyOptions(config);
1924
- const updateOpts = {
1925
- ...opts,
1926
- configId: config.configid,
1927
- name: config.configname,
1928
- optional: {
1929
- serve: config.serve
1930
- }
1931
- };
1932
- if (noWait) {
1933
- const existing = this.tunnelManager.getManagedTunnel(config.configid);
1934
- if (!existing.tunnelConfig) throw new Error("Invalid tunnel state before configuration update");
1935
- this.tunnelManager.updateConfig(updateOpts).catch((err) => {
1936
- logger.error("No-wait updateConfig failed", { configid: config.configid, err: String(err) });
1937
- });
1938
- return this.buildPendingTunnelResponse(existing.tunnelid, existing.tunnelConfig, config.configid, existing.tunnelName, existing.serve);
1939
- }
1940
- const tunnel = await this.tunnelManager.updateConfig(updateOpts);
1941
- if (!tunnel.instance || !tunnel.tunnelConfig)
1942
- throw new Error("Invalid tunnel state after configuration update");
1943
- return this.buildTunnelResponse(tunnel.tunnelid, tunnel.tunnelConfig, config.configid, tunnel.tunnelName, tunnel.serve);
1944
- } catch (err) {
1945
- return this.error(ErrorCode.InternalServerError, err, "Failed to update tunnel configuration");
1946
- }
1947
- }
1948
- async handleUpdateConfigV2(config, noWait = false) {
1949
- try {
1950
- if (noWait) {
1951
- const existing = this.tunnelManager.getManagedTunnel(config.configId);
1952
- console.log(existing);
1953
- if (!existing.tunnelConfig) throw new Error("Invalid tunnel state before configuration update");
1954
- this.tunnelManager.updateConfig(config).catch((err) => {
1955
- logger.error("No-wait updateConfigV2 failed", { configId: config.configId, err: String(err) });
1956
- });
1957
- return this.buildPendingTunnelResponseV2(existing.tunnelid, existing.tunnelConfig, config, config.configId, existing.tunnelName, existing.serve);
1958
- }
1959
- const tunnel = await this.tunnelManager.updateConfig(config);
1960
- if (!tunnel.instance || !tunnel.tunnelConfig)
1961
- throw new Error("Invalid tunnel state after configuration update");
1962
- return this.buildTunnelResponseV2(tunnel.tunnelid, tunnel.tunnelConfig, config, config.configId, tunnel.tunnelName, tunnel.serve);
1963
- } catch (err) {
1964
- return this.error(ErrorCode.InternalServerError, err, "Failed to update tunnel configuration");
1965
- }
1966
- }
1967
- async handleListV2() {
1968
- try {
1969
- const tunnels = await this.tunnelManager.getAllTunnels();
1970
- if (tunnels.length === 0) {
1971
- return [];
1972
- }
1973
- return Promise.all(
1974
- tunnels.map(async (t) => {
1975
- const rawStats = this.tunnelManager.getLatestTunnelStats(t.tunnelid) || newStats();
1976
- const [status, tlsInfo, greetMsg] = await Promise.all([
1977
- this.tunnelManager.getTunnelStatus(t.tunnelid),
1978
- this.tunnelManager.getLocalserverTlsInfo(t.tunnelid),
1979
- this.tunnelManager.getTunnelGreetMessage(t.tunnelid)
1980
- ]);
1981
- const tunnelConfguration = status !== "closed" /* Closed */ && status !== "exited" /* Exited */ ? await this.tunnelManager.getTunnelConfig("", t.tunnelid) : t.tunnelConfig;
1982
- const tunnelConfig = pinggyOptionsToTunnelConfigV1(tunnelConfguration, t.tunnelConfig);
1983
- return {
1984
- tunnelid: t.tunnelid,
1985
- remoteurls: t.remoteurls,
1986
- status: this.buildStatus(t.tunnelid, status, "" /* NoError */),
1987
- stats: rawStats,
1988
- tunnelconfig: tunnelConfig,
1989
- greetmsg: greetMsg
1990
- };
1991
- })
1992
- );
1993
- } catch (err) {
1994
- return this.error(ErrorCode.InternalServerError, err, "Failed to list tunnels");
1995
- }
1996
- }
1997
- async handleList() {
1998
- try {
1999
- const tunnels = await this.tunnelManager.getAllTunnels();
2000
- if (tunnels.length === 0) {
2001
- return [];
2002
- }
2003
- return Promise.all(
2004
- tunnels.map(async (t) => {
2005
- const rawStats = this.tunnelManager.getLatestTunnelStats(t.tunnelid) || newStats();
2006
- const [status, tlsInfo, greetMsg] = await Promise.all([
2007
- this.tunnelManager.getTunnelStatus(t.tunnelid),
2008
- this.tunnelManager.getLocalserverTlsInfo(t.tunnelid),
2009
- this.tunnelManager.getTunnelGreetMessage(t.tunnelid)
2010
- ]);
2011
- const pinggyOptions = status !== "closed" /* Closed */ && status !== "exited" /* Exited */ ? await this.tunnelManager.getTunnelConfig("", t.tunnelid) : t.tunnelConfig;
2012
- const tunnelConfig = pinggyOptionsToTunnelConfig(pinggyOptions, t.configId, t.tunnelName, tlsInfo, greetMsg, t.serve);
2013
- return {
2014
- tunnelid: t.tunnelid,
2015
- remoteurls: t.remoteurls,
2016
- status: this.buildStatus(t.tunnelid, status, "" /* NoError */),
2017
- stats: rawStats,
2018
- tunnelconfig: tunnelConfig
2019
- };
2020
- })
2021
- );
2022
- } catch (err) {
2023
- return this.error(ErrorCode.InternalServerError, err, "Failed to list tunnels");
2024
- }
2025
- }
2026
- async handleStop(tunnelid) {
2027
- try {
2028
- const { configId } = this.tunnelManager.stopTunnel(tunnelid);
2029
- const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
2030
- if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
2031
- return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, configId, managed.tunnelName, managed.serve);
2032
- } catch (err) {
2033
- return this.error(ErrorCode.TunnelNotFound, err, "Failed to stop tunnel");
2034
- }
2035
- }
2036
- async handleGet(tunnelid) {
2037
- try {
2038
- const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
2039
- if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
2040
- return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, managed.configId, managed.tunnelName, managed.serve);
2041
- } catch (err) {
2042
- return this.error(ErrorCode.TunnelNotFound, err, "Failed to get tunnel information");
2043
- }
2044
- }
2045
- async handleRestart(tunnelid, noWait = false) {
2046
- try {
2047
- if (noWait) {
2048
- const managed2 = this.tunnelManager.getManagedTunnel("", tunnelid);
2049
- if (!managed2?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
2050
- this.tunnelManager.restartTunnel(tunnelid).catch((err) => {
2051
- logger.error("No-wait restartTunnel failed", { tunnelid, err: String(err) });
2052
- });
2053
- return this.buildPendingTunnelResponse(tunnelid, managed2.tunnelConfig, managed2.configId, managed2.tunnelName, managed2.serve);
2054
- }
2055
- await this.tunnelManager.restartTunnel(tunnelid);
2056
- const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
2057
- if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
2058
- return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, managed.configId, managed.tunnelName, managed.serve);
2059
- } catch (err) {
2060
- return this.error(ErrorCode.TunnelNotFound, err, "Failed to restart tunnel");
2061
- }
2062
- }
2063
- handleRegisterStatsListener(tunnelid, listener) {
2064
- this.tunnelManager.registerStatsListener(tunnelid, listener);
2065
- }
2066
- handleUnregisterStatsListener(tunnelid, listnerId) {
2067
- this.tunnelManager.deregisterStatsListener(tunnelid, listnerId);
2068
- }
2069
- handleGetTunnelStats(tunnelid) {
2070
- try {
2071
- const stats = this.tunnelManager.getTunnelStats(tunnelid);
2072
- if (!stats) {
2073
- return [newStats()];
2074
- }
2075
- return stats;
2076
- } catch (err) {
2077
- return this.error(ErrorCode.TunnelNotFound, err, "Failed to get tunnel stats");
2078
- }
2079
- }
2080
- handleRegisterDisconnectListener(tunnelid, listener) {
2081
- this.tunnelManager.registerDisconnectListener(tunnelid, listener);
2082
- }
2083
- handleRemoveStoppedTunnelByConfigId(configId) {
2084
- try {
2085
- return this.tunnelManager.removeStoppedTunnelByConfigId(configId);
2086
- } catch (err) {
2087
- return this.error(ErrorCode.InternalServerError, err, "Failed to remove stopped tunnel by configId");
2088
- }
2089
- }
2090
- handleRemoveStoppedTunnelByTunnelId(tunnelId) {
2091
- try {
2092
- return this.tunnelManager.removeStoppedTunnelByTunnelId(tunnelId);
2093
- } catch (err) {
2094
- return this.error(ErrorCode.InternalServerError, err, "Failed to remove stopped tunnel by tunnelId");
2095
- }
2096
- }
2097
- };
2098
-
2099
- // src/remote_management/remoteManagement.ts
2100
- import WebSocket from "ws";
2101
-
2102
- // src/remote_management/websocket_printer.ts
2103
- import pico3 from "picocolors";
2104
- var PENDING_START_TIMEOUT_MS = 5 * 60 * 1e3;
2105
- var RemoteManagementWebSocketPrinter = class {
2106
- constructor() {
2107
- this.tunnelManager = TunnelManager.getInstance();
2108
- this.pendingStarts = /* @__PURE__ */ new Map();
2109
- }
2110
- setTunnelHandler(tunnelHandler) {
2111
- this.tunnelHandler = tunnelHandler;
2112
- }
2113
- queueStart(config) {
2114
- this.cleanupExpiredPendingStarts();
2115
- const entry = {
2116
- configId: this.getConfigIdFromRequest(config),
2117
- configName: this.getConfigNameFromRequest(config),
2118
- queuedAt: Date.now()
2119
- };
2120
- this.latestPendingConfigId = entry.configId;
2121
- this.pendingStarts.set(entry.configId, entry);
2122
- printer_default.startSpinner("Starting tunnel with config name: " + entry.configName);
2123
- }
2124
- failQueuedStart(config, reason) {
2125
- const configId = this.getConfigIdFromRequest(config);
2126
- const pending = this.pendingStarts.get(configId);
2127
- const configName = pending?.configName || this.getConfigNameFromRequest(config);
2128
- this.pendingStarts.delete(configId);
2129
- if (this.latestPendingConfigId === configId) {
2130
- this.latestPendingConfigId = void 0;
2131
- printer_default.stopSpinnerFail(`Failed to start tunnel with config name: ${configName}. ${reason}`);
2132
- }
2133
- }
2134
- handleStartResult(config, result) {
2135
- this.cleanupExpiredPendingStarts();
2136
- const requestedConfigId = this.getConfigIdFromRequest(config);
2137
- if (this.latestPendingConfigId && requestedConfigId !== this.latestPendingConfigId) {
2138
- this.pendingStarts.delete(requestedConfigId);
2139
- return;
2140
- }
2141
- if (isErrorResponse(result)) {
2142
- this.failQueuedStart(config, result.message);
2143
- return;
2144
- }
2145
- const configId = this.getConfigIdFromTunnel(result);
2146
- const pending = this.pendingStarts.get(requestedConfigId) || {
2147
- configId: requestedConfigId,
2148
- configName: this.getConfigNameFromRequest(config),
2149
- queuedAt: Date.now()
2150
- };
2151
- pending.tunnelId = result.tunnelid;
2152
- this.pendingStarts.set(requestedConfigId, pending);
2153
- if (result.remoteurls.length > 0) {
2154
- this.completePendingStart(pending, result.remoteurls);
2155
- }
2156
- }
2157
- printStopRequested(tunnelId) {
2158
- const details = this.resolveTunnelDetails(tunnelId);
2159
- printer_default.startSpinner("Stopping tunnel with config name: " + details.configName);
2160
- }
2161
- handleStopResult(tunnelId, result) {
2162
- const details = this.resolveTunnelDetails(tunnelId, result);
2163
- if (isErrorResponse(result)) {
2164
- printer_default.stopSpinnerFail("Failed to stop tunnel with config name: " + details.configName);
2165
- return;
2166
- }
2167
- this.pendingStarts.delete(details.configId);
2168
- printer_default.stopSpinnerSuccess("Stopped tunnel with config name: " + details.configName);
2169
- }
2170
- printRestartRequested(tunnelId) {
2171
- const details = this.resolveTunnelDetails(tunnelId);
2172
- printer_default.startSpinner("Restarting tunnel with config name: " + details.configName);
2173
- }
2174
- handleRestartResult(tunnelId, result) {
2175
- const details = this.resolveTunnelDetails(tunnelId, result);
2176
- if (isErrorResponse(result)) {
2177
- printer_default.warn(`Failed to restart tunnel with config name: ${details.configName}. ${result.message}`);
2178
- printer_default.stopSpinnerFail("Failed to restart tunnel with config name: " + details.configName);
2179
- return;
2180
- }
2181
- printer_default.stopSpinnerSuccess("Restarted tunnel with config name: " + details.configName);
2182
- if (result.remoteurls?.length > 0) {
2183
- printer_default.info(pico3.cyanBright("Remote URLs:"));
2184
- (result.remoteurls ?? []).forEach(
2185
- (url) => printer_default.print(" " + pico3.magentaBright(url))
2186
- );
2187
- }
2188
- }
2189
- monitorList(result) {
2190
- this.cleanupExpiredPendingStarts();
2191
- if (!Array.isArray(result) || this.pendingStarts.size === 0 || !this.latestPendingConfigId) {
2192
- return;
2193
- }
2194
- for (const tunnel of result) {
2195
- const pending = this.findPendingStart(tunnel);
2196
- if (!pending) {
2197
- continue;
2198
- }
2199
- if (pending.configId !== this.latestPendingConfigId) {
2200
- continue;
2201
- }
2202
- pending.tunnelId = tunnel.tunnelid;
2203
- this.pendingStarts.set(pending.configId, pending);
2204
- if (tunnel.remoteurls.length > 0) {
2205
- this.completePendingStart(pending, tunnel.remoteurls);
2206
- continue;
2207
- }
2208
- if (tunnel.status.state === "exited" /* Exited */) {
2209
- const reason = tunnel.status.errormsg || "Tunnel exited before a public URL was assigned";
2210
- this.pendingStarts.delete(pending.configId);
2211
- this.latestPendingConfigId = void 0;
2212
- printer_default.stopSpinnerFail(`Tunnel start did not complete for config name: ${pending.configName}. ${reason}`);
2213
- }
2214
- }
2215
- }
2216
- completePendingStart(entry, urls) {
2217
- if (this.latestPendingConfigId && entry.configId !== this.latestPendingConfigId) {
2218
- this.pendingStarts.delete(entry.configId);
2219
- return;
2220
- }
2221
- this.pendingStarts.delete(entry.configId);
2222
- this.latestPendingConfigId = void 0;
2223
- printer_default.stopSpinnerSuccess(`Tunnel started with config name: ${entry.configName}.`);
2224
- printer_default.info(pico3.cyanBright("Remote URLs:"));
2225
- (urls ?? []).forEach(
2226
- (url) => printer_default.print(" " + pico3.magentaBright(url))
2227
- );
2228
- }
2229
- cleanupExpiredPendingStarts() {
2230
- const now = Date.now();
2231
- for (const [configId, entry] of this.pendingStarts.entries()) {
2232
- if (now - entry.queuedAt <= PENDING_START_TIMEOUT_MS) {
2233
- continue;
2234
- }
2235
- this.pendingStarts.delete(configId);
2236
- printer_default.warn(`Timed out while waiting for tunnel URL for config name: ${entry.configName}`);
2237
- logger.warn("Pending websocket start entry expired", { configId, tunnelId: entry.tunnelId });
2238
- }
2239
- }
2240
- findPendingStart(tunnel) {
2241
- const configId = this.getConfigIdFromTunnel(tunnel);
2242
- const byConfigId = this.pendingStarts.get(configId);
2243
- if (byConfigId) {
2244
- return byConfigId;
2245
- }
2246
- for (const entry of this.pendingStarts.values()) {
2247
- if (entry.tunnelId === tunnel.tunnelid) {
2248
- return entry;
2249
- }
2250
- }
2251
- return void 0;
2252
- }
2253
- resolveTunnelDetails(tunnelId, result) {
2254
- try {
2255
- const managed = this.tunnelManager.getManagedTunnel(void 0, tunnelId);
2256
- return {
2257
- configId: managed.configId,
2258
- configName: managed.tunnelName || managed.configId || tunnelId
2259
- };
2260
- } catch {
2261
- if (result && !isErrorResponse(result)) {
2262
- return {
2263
- configId: this.getConfigIdFromTunnel(result),
2264
- configName: this.getConfigNameFromTunnel(result)
2265
- };
2266
- }
2267
- return {
2268
- configId: tunnelId,
2269
- configName: tunnelId
2270
- };
2271
- }
2272
- }
2273
- getConfigIdFromRequest(config) {
2274
- return "configid" in config ? config.configid : config.configId;
2275
- }
2276
- getConfigNameFromRequest(config) {
2277
- return "configname" in config ? config.configname : config.name;
2278
- }
2279
- getConfigIdFromTunnel(tunnel) {
2280
- return "configid" in tunnel.tunnelconfig ? tunnel.tunnelconfig.configid : tunnel.tunnelconfig.configId;
2281
- }
2282
- getConfigNameFromTunnel(tunnel) {
2283
- return "configname" in tunnel.tunnelconfig ? tunnel.tunnelconfig.configname : tunnel.tunnelconfig.name;
2284
- }
2285
- };
2286
- var remoteManagementWebSocketPrinter = new RemoteManagementWebSocketPrinter();
2287
-
2288
- // src/remote_management/websocket_handlers.ts
2289
- import z2 from "zod";
2290
- var WebSocketCommandHandler = class {
2291
- constructor() {
2292
- this.tunnelHandler = new TunnelOperations();
2293
- remoteManagementWebSocketPrinter.setTunnelHandler(this.tunnelHandler);
2294
- }
2295
- safeParse(text) {
2296
- if (!text) return void 0;
2297
- try {
2298
- return JSON.parse(text);
2299
- } catch (e) {
2300
- logger.warn("Invalid JSON payload", { error: String(e), text });
2301
- return void 0;
2302
- }
2303
- }
2304
- sendResponse(ws, resp) {
2305
- const payload = {
2306
- ...resp,
2307
- response: Buffer.from(resp.response || []).toString("base64")
2308
- };
2309
- ws.send(JSON.stringify(payload));
2310
- }
2311
- sendError(ws, req, message, code = ErrorCode.InternalServerError) {
2312
- const resp = NewErrorResponseObject({ code, message });
2313
- resp.command = req.command || "";
2314
- resp.requestid = req.requestid || "";
2315
- this.sendResponse(ws, resp);
2316
- }
2317
- async handleStartReq(req, raw) {
2318
- let queuedConfig;
2319
- try {
2320
- const dc = StartSchema.parse(raw);
2321
- queuedConfig = dc.tunnelConfig;
2322
- remoteManagementWebSocketPrinter.queueStart(dc.tunnelConfig);
2323
- const result = await this.tunnelHandler.handleStart(dc.tunnelConfig, true);
2324
- remoteManagementWebSocketPrinter.handleStartResult(dc.tunnelConfig, result);
2325
- return this.wrapResponse(result, req);
2326
- } catch (e) {
2327
- if (queuedConfig) {
2328
- remoteManagementWebSocketPrinter.failQueuedStart(queuedConfig, String(e));
2329
- }
2330
- if (e instanceof z2.ZodError) {
2331
- printer_default.warn("Validation failed for start request");
2332
- return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
2333
- }
2334
- printer_default.warn(`Error in handleStartReq error: ${String(e)}`);
2335
- return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
2336
- }
2337
- }
2338
- async handleStartV2Req(req, raw) {
2339
- let queuedConfig;
2340
- try {
2341
- const dc = StartV2Schema.parse(raw);
2342
- queuedConfig = dc.tunnelConfig;
2343
- remoteManagementWebSocketPrinter.queueStart(dc.tunnelConfig);
2344
- const result = await this.tunnelHandler.handleStartV2(dc.tunnelConfig, true);
2345
- remoteManagementWebSocketPrinter.handleStartResult(dc.tunnelConfig, result);
2346
- return this.wrapResponse(result, req);
2347
- } catch (e) {
2348
- if (queuedConfig) {
2349
- remoteManagementWebSocketPrinter.failQueuedStart(queuedConfig, String(e));
2350
- }
2351
- if (e instanceof z2.ZodError) {
2352
- printer_default.warn("Validation failed for start-v2 request");
2353
- return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
2354
- }
2355
- printer_default.warn(`Error in handleStartV2Req error: ${String(e)}`);
2356
- return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
2357
- }
2358
- }
2359
- async handleStopReq(req, raw) {
2360
- try {
2361
- const dc = StopSchema.parse(raw);
2362
- remoteManagementWebSocketPrinter.printStopRequested(dc.tunnelID);
2363
- const result = await this.tunnelHandler.handleStop(dc.tunnelID);
2364
- remoteManagementWebSocketPrinter.handleStopResult(dc.tunnelID, result);
2365
- return this.wrapResponse(result, req);
2366
- } catch (e) {
2367
- if (e instanceof z2.ZodError) {
2368
- printer_default.warn("Validation failed for stop request");
2369
- return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
2370
- }
2371
- printer_default.warn(`Error in handleStopReq error: ${String(e)}`);
2372
- return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
2373
- }
2374
- }
2375
- async handleGetReq(req, raw) {
2376
- try {
2377
- const dc = GetSchema.parse(raw);
2378
- const result = await this.tunnelHandler.handleGet(dc.tunnelID);
2379
- return this.wrapResponse(result, req);
2380
- } catch (e) {
2381
- if (e instanceof z2.ZodError) {
2382
- printer_default.warn("Validation failed for get request");
2383
- return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
2384
- }
2385
- printer_default.warn(`Error in handleGetReq error: ${String(e)}`);
2386
- return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
2387
- }
2388
- }
2389
- async handleRestartReq(req, raw) {
2390
- try {
2391
- const dc = RestartSchema.parse(raw);
2392
- remoteManagementWebSocketPrinter.printRestartRequested(dc.tunnelID);
2393
- const result = await this.tunnelHandler.handleRestart(dc.tunnelID, true);
2394
- remoteManagementWebSocketPrinter.handleRestartResult(dc.tunnelID, result);
2395
- return this.wrapResponse(result, req);
2396
- } catch (e) {
2397
- if (e instanceof z2.ZodError) {
2398
- printer_default.warn("Validation failed for restart request");
2399
- return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
2400
- }
2401
- printer_default.warn(`Error in handleRestartReq error: ${String(e)}`);
2402
- return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
2403
- }
2404
- }
2405
- async handleUpdateConfigReq(req, raw) {
2406
- try {
2407
- const dc = UpdateConfigSchema.parse(raw);
2408
- const result = await this.tunnelHandler.handleUpdateConfig(dc.tunnelConfig, true);
2409
- return this.wrapResponse(result, req);
2410
- } catch (e) {
2411
- if (e instanceof z2.ZodError) {
2412
- printer_default.warn("Validation failed for updateconfig request");
2413
- return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
2414
- }
2415
- printer_default.warn(`Error in handleUpdateConfigReq error: ${String(e)}`);
2416
- return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
2417
- }
2418
- }
2419
- async handleUpdateConfigV2Req(req, raw) {
2420
- try {
2421
- const dc = UpdateConfigV2Schema.parse(raw);
2422
- const result = await this.tunnelHandler.handleUpdateConfigV2(dc.tunnelConfig, true);
2423
- return this.wrapResponse(result, req);
2424
- } catch (e) {
2425
- if (e instanceof z2.ZodError) {
2426
- printer_default.warn("Validation failed for update-config-v2 request");
2427
- return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
2428
- }
2429
- printer_default.warn(`Error in handleUpdateConfigV2Req error: ${String(e)}`);
2430
- return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
2431
- }
2432
- }
2433
- async handleListReq(req) {
2434
- try {
2435
- const result = await this.tunnelHandler.handleList();
2436
- remoteManagementWebSocketPrinter.monitorList(result);
2437
- return this.wrapResponse(result, req);
2438
- } catch (e) {
2439
- printer_default.warn(`Error in handleListReq error: ${String(e)}`);
2440
- return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
2441
- }
2442
- }
2443
- async handleListV2Req(req) {
2444
- try {
2445
- const result = await this.tunnelHandler.handleListV2();
2446
- remoteManagementWebSocketPrinter.monitorList(result);
2447
- return this.wrapResponse(result, req);
2448
- } catch (e) {
2449
- printer_default.warn(`Error in handleListV2Req error: ${String(e)}`);
2450
- return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
2451
- }
2452
- }
2453
- async handleGetVersionReq(ws, req) {
2454
- try {
2455
- const versionResponse = {
2456
- cli_version: getVersion()
2457
- };
2458
- const payload = {
2459
- command: req.command,
2460
- requestid: req.requestid,
2461
- response: JSON.stringify(versionResponse),
2462
- error: false
2463
- };
2464
- ws.send(JSON.stringify(payload));
2465
- } catch (e) {
2466
- printer_default.warn(`Error in handleGetVersionReq error: ${String(e)}`);
2467
- this.sendError(ws, req, String(e));
2468
- }
2469
- }
2470
- wrapResponse(result, req) {
2471
- if (isErrorResponse(result)) {
2472
- const errResp = NewErrorResponseObject(result);
2473
- errResp.command = req.command;
2474
- errResp.requestid = req.requestid;
2475
- return errResp;
2476
- }
2477
- const finalResult = JSON.parse(JSON.stringify(result));
2478
- if (Array.isArray(finalResult)) {
2479
- finalResult.forEach((item) => {
2480
- if (item?.tunnelconfig) {
2481
- delete item.tunnelconfig.allowPreflight;
2482
- }
2483
- });
2484
- } else if (finalResult?.tunnelconfig) {
2485
- delete finalResult.tunnelconfig.allowPreflight;
2486
- }
2487
- const respObj = NewResponseObject(finalResult);
2488
- respObj.command = req.command;
2489
- respObj.requestid = req.requestid;
2490
- return respObj;
2491
- }
2492
- async handle(ws, req) {
2493
- const cmd = (req.command || "").toLowerCase();
2494
- const raw = this.safeParse(req.data);
2495
- try {
2496
- let response;
2497
- switch (cmd) {
2498
- case "start": {
2499
- response = await this.handleStartReq(req, raw);
2500
- break;
2501
- }
2502
- case "start-v2": {
2503
- response = await this.handleStartV2Req(req, raw);
2504
- break;
2505
- }
2506
- case "stop": {
2507
- response = await this.handleStopReq(req, raw);
2508
- break;
2509
- }
2510
- case "get": {
2511
- response = await this.handleGetReq(req, raw);
2512
- break;
2513
- }
2514
- case "restart": {
2515
- response = await this.handleRestartReq(req, raw);
2516
- break;
2517
- }
2518
- case "updateconfig": {
2519
- response = await this.handleUpdateConfigReq(req, raw);
2520
- break;
2521
- }
2522
- case "update-config-v2": {
2523
- response = await this.handleUpdateConfigV2Req(req, raw);
2524
- break;
2525
- }
2526
- case "list": {
2527
- response = await this.handleListReq(req);
2528
- break;
2529
- }
2530
- case "list-v2": {
2531
- response = await this.handleListV2Req(req);
2532
- break;
2533
- }
2534
- case "get-version": {
2535
- await this.handleGetVersionReq(ws, req);
2536
- return;
2537
- }
2538
- default:
2539
- if (typeof req.command === "string") {
2540
- logger.warn("Unknown command", { command: req.command });
2541
- }
2542
- return this.sendError(ws, req, "Invalid command");
2543
- }
2544
- logger.debug("Sending response", { command: response.command, requestid: response.requestid });
2545
- this.sendResponse(ws, response);
2546
- } catch (e) {
2547
- if (e instanceof z2.ZodError) {
2548
- logger.warn("Validation failed", { cmd, issues: e.issues });
2549
- return this.sendError(ws, req, "Invalid request data", ErrorCode.InvalidBodyFormatError);
2550
- }
2551
- logger.error("Error handling command", { cmd, error: String(e) });
2552
- return this.sendError(ws, req, e?.message || "Internal error");
2553
- }
2554
- }
2555
- };
2556
- function sendVersionResponse(ws) {
2557
- const versionResponse = {
2558
- cli_version: getVersion()
2559
- };
2560
- const payload = {
2561
- command: "get-version",
2562
- requestid: "0",
2563
- response: JSON.stringify(versionResponse),
2564
- error: false
2565
- };
2566
- ws.send(JSON.stringify(payload));
2567
- }
2568
- function handleConnectionStatusMessage(firstMessage) {
2569
- try {
2570
- const text = typeof firstMessage === "string" ? firstMessage : firstMessage.toString();
2571
- const cs = JSON.parse(text);
2572
- if (!cs.success) {
2573
- const msg = cs.error_msg || "Connection failed";
2574
- printer_default.warn(`Connection failed: ${msg}`);
2575
- logger.warn("Remote management connection failed", { error_code: cs.error_code, error_msg: msg });
2576
- return false;
2577
- }
2578
- return true;
2579
- } catch (e) {
2580
- logger.warn("Failed to parse connection status message", { error: String(e) });
2581
- return true;
2582
- }
2583
- }
2584
-
2585
- // src/remote_management/remoteManagement.ts
2586
- var RECONNECT_SLEEP_MS = 5e3;
2587
- var PING_INTERVAL_MS = 3e4;
2588
- var RemoteManagementUnauthorizedError = class extends Error {
2589
- constructor() {
2590
- super("Unauthorized. Please enter a valid token.");
2591
- this.name = "RemoteManagementUnauthorizedError";
2592
- }
2593
- };
2594
- var _remoteManagementState = {
2595
- status: "NOT_RUNNING",
2596
- errorMessage: ""
2597
- };
2598
- var _stopRequested = false;
2599
- var currentWs = null;
2600
- function buildRemoteManagementWsUrl(manage) {
2601
- let baseUrl = (manage || "dashboard.pinggy.io").trim();
2602
- if (!(baseUrl.startsWith("ws://") || baseUrl.startsWith("wss://"))) {
2603
- baseUrl = "wss://" + baseUrl;
2604
- }
2605
- const trimmed = baseUrl.replace(/\/$/, "");
2606
- return `${trimmed}/backend/api/v1/remote-management/connect`;
2607
- }
2608
- function extractHostname(u) {
2609
- try {
2610
- const url = new URL(u);
2611
- return url.host;
2612
- } catch {
2613
- return u;
2614
- }
2615
- }
2616
- function sleep(ms) {
2617
- return new Promise((res) => setTimeout(res, ms));
2618
- }
2619
- async function parseRemoteManagement(values) {
2620
- const rmToken = values["remote-management"];
2621
- if (typeof rmToken === "string" && rmToken.trim().length > 0) {
2622
- const manageHost = values["manage"];
2623
- try {
2624
- const remoteManagementConfig = {
2625
- apiKey: rmToken,
2626
- serverUrl: buildRemoteManagementWsUrl(manageHost)
2627
- };
2628
- await initiateRemoteManagement(remoteManagementConfig);
2629
- return { ok: true };
2630
- } catch (e) {
2631
- logger.error("Failed to initiate remote management:", e);
2632
- return { ok: false, error: e };
2633
- }
2634
- }
2635
- }
2636
- async function initiateRemoteManagement(remoteManagementConfig) {
2637
- if (!remoteManagementConfig.apiKey || remoteManagementConfig.apiKey.trim().length === 0) {
2638
- throw new Error("Remote management token is required (use --remote-management <TOKEN>)");
2639
- }
2640
- const wsUrl = remoteManagementConfig.serverUrl;
2641
- const wsHost = extractHostname(wsUrl);
2642
- logger.info("Remote management mode enabled.");
2643
- _stopRequested = false;
2644
- const sigintHandler = () => {
2645
- _stopRequested = true;
2646
- };
2647
- process.once("SIGINT", sigintHandler);
2648
- const logConnecting = () => {
2649
- printer_default.print(`Connecting to ${wsHost}`);
2650
- logger.info("Connecting to remote management", { wsUrl });
2651
- };
2652
- while (!_stopRequested) {
2653
- logConnecting();
2654
- setRemoteManagementState({ status: RemoteManagementStatus.Connecting, errorMessage: "" });
2655
- try {
2656
- await handleWebSocketConnection(wsUrl, wsHost, remoteManagementConfig.apiKey);
2657
- } catch (error) {
2658
- if (error instanceof RemoteManagementUnauthorizedError) {
2659
- throw error;
2660
- }
2661
- logger.warn("Remote management connection error", { error: String(error) });
2662
- }
2663
- if (_stopRequested) {
2664
- break;
2665
- }
2666
- printer_default.warn(`Remote management disconnected. Reconnecting in ${RECONNECT_SLEEP_MS / 1e3} seconds...`);
2667
- logger.info("Reconnecting to remote management after disconnect");
2668
- await sleep(RECONNECT_SLEEP_MS);
2669
- }
2670
- process.removeListener("SIGINT", sigintHandler);
2671
- logger.info("Remote management stopped.");
2672
- return getRemoteManagementState();
2673
- }
2674
- async function handleWebSocketConnection(wsUrl, wsHost, token, onOpenCallback) {
2675
- return new Promise((resolve, reject) => {
2676
- const ws = new WebSocket(wsUrl, {
2677
- headers: { Authorization: `Bearer ${token}` }
2678
- });
2679
- currentWs = ws;
2680
- let heartbeat = null;
2681
- let firstMessage = true;
2682
- let settled = false;
2683
- const cleanup = (err) => {
2684
- if (settled) {
2685
- return;
2686
- }
2687
- settled = true;
2688
- if (heartbeat) {
2689
- clearInterval(heartbeat);
2690
- }
2691
- currentWs = null;
2692
- if (err) {
2693
- reject(err);
2694
- } else {
2695
- resolve();
2696
- }
2697
- };
2698
- ws.once("open", () => {
2699
- printer_default.success(`Connected to ${wsHost}`);
2700
- setRemoteManagementState({ status: RemoteManagementStatus.Running, errorMessage: "" });
2701
- onOpenCallback?.();
2702
- heartbeat = setInterval(() => {
2703
- if (ws.readyState === WebSocket.OPEN) ws.ping();
2704
- }, PING_INTERVAL_MS);
2705
- });
2706
- ws.on("ping", () => ws.pong());
2707
- ws.on("message", async (data) => {
2708
- try {
2709
- if (firstMessage) {
2710
- firstMessage = false;
2711
- const ok = handleConnectionStatusMessage(data);
2712
- if (!ok) {
2713
- ws.close();
2714
- return;
2715
- }
2716
- sendVersionResponse(ws);
2717
- return;
2718
- }
2719
- setRemoteManagementState({ status: RemoteManagementStatus.Running, errorMessage: "" });
2720
- const req = JSON.parse(data.toString("utf8"));
2721
- await new WebSocketCommandHandler().handle(ws, req);
2722
- } catch (e) {
2723
- logger.warn("Failed handling websocket message", { error: String(e) });
2724
- }
2725
- });
2726
- ws.on("unexpected-response", (_, res) => {
2727
- if (res.statusCode === 401) {
2728
- setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: `HTTP ${res.statusCode}` });
2729
- logger.error("Unauthorized (401) on remote management connect");
2730
- cleanup(new RemoteManagementUnauthorizedError());
2731
- ws.close();
2732
- } else {
2733
- logger.warn("Unexpected HTTP response ", { statusCode: res.statusCode });
2734
- printer_default.warn(`Unexpected HTTP ${res.statusCode}. Retrying...`);
2735
- cleanup();
2736
- ws.close();
2737
- }
2738
- });
2739
- ws.on("close", (code, reason) => {
2740
- setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: "" });
2741
- logger.info("WebSocket closed", { code, reason: reason.toString() });
2742
- printer_default.warn(`Disconnected (code: ${code}). Retrying in ${RECONNECT_SLEEP_MS / 1e3}s...`);
2743
- cleanup();
2744
- });
2745
- ws.on("error", (err) => {
2746
- setRemoteManagementState({ status: RemoteManagementStatus.Error, errorMessage: err.message });
2747
- logger.warn("WebSocket error", { error: err.message });
2748
- printer_default.warn(err.message);
2749
- cleanup();
2750
- });
2751
- });
2752
- }
2753
- async function closeRemoteManagement(timeoutMs = 1e4) {
2754
- _stopRequested = true;
2755
- try {
2756
- if (currentWs) {
2757
- try {
2758
- setRemoteManagementState({ status: RemoteManagementStatus.Disconnecting, errorMessage: "" });
2759
- currentWs.close();
2760
- } catch (e) {
2761
- logger.warn("Error while closing current remote management websocket", { error: String(e) });
2762
- }
2763
- }
2764
- const start = Date.now();
2765
- while (_remoteManagementState.status === "RUNNING") {
2766
- if (Date.now() - start > timeoutMs) {
2767
- logger.warn("Timed out waiting for remote management to stop");
2768
- break;
2769
- }
2770
- await sleep(200);
2771
- }
2772
- } finally {
2773
- currentWs = null;
2774
- setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: "" });
2775
- return getRemoteManagementState();
2776
- }
2777
- }
2778
- function startRemoteManagement(remoteManagementConfig) {
2779
- if (!remoteManagementConfig.apiKey || remoteManagementConfig.apiKey.trim().length === 0) {
2780
- return Promise.reject(new Error("Remote management token is required"));
2781
- }
2782
- const wsUrl = remoteManagementConfig.serverUrl;
2783
- const wsHost = extractHostname(wsUrl);
2784
- logger.info("Remote management mode enabled.");
2785
- _stopRequested = false;
2786
- return new Promise((resolve, reject) => {
2787
- let firstSettled = false;
2788
- const settleOnce = (err) => {
2789
- if (firstSettled) {
2790
- return;
2791
- }
2792
- firstSettled = true;
2793
- if (err) {
2794
- reject(err);
2795
- } else {
2796
- resolve(getRemoteManagementState());
2797
- }
2798
- };
2799
- const runLoop = async () => {
2800
- const sigintHandler = () => {
2801
- _stopRequested = true;
2802
- };
2803
- process.once("SIGINT", sigintHandler);
2804
- while (!_stopRequested) {
2805
- logger.info("Connecting to remote management", { wsUrl });
2806
- setRemoteManagementState({ status: RemoteManagementStatus.Connecting, errorMessage: "" });
2807
- try {
2808
- await handleWebSocketConnection(wsUrl, wsHost, remoteManagementConfig.apiKey, () => settleOnce());
2809
- } catch (error) {
2810
- if (error instanceof RemoteManagementUnauthorizedError) {
2811
- settleOnce(error);
2812
- process.removeListener("SIGINT", sigintHandler);
2813
- return;
2814
- }
2815
- settleOnce();
2816
- logger.warn("Remote management connection error", { error: String(error) });
2817
- }
2818
- if (_stopRequested) {
2819
- break;
2820
- }
2821
- logger.info("Reconnecting to remote management after disconnect");
2822
- await sleep(RECONNECT_SLEEP_MS);
2823
- }
2824
- process.removeListener("SIGINT", sigintHandler);
2825
- logger.info("Remote management stopped.");
2826
- };
2827
- runLoop().catch((err) => settleOnce(err instanceof Error ? err : new Error(String(err))));
2828
- });
2829
- }
2830
- function getRemoteManagementState() {
2831
- return _remoteManagementState;
2832
- }
2833
- function setRemoteManagementState(state, errorMessage) {
2834
- _remoteManagementState = {
2835
- status: state.status,
2836
- errorMessage: errorMessage || ""
2837
- };
2838
- }
2839
-
2840
- export {
2841
- printer_default,
2842
- getRandomId,
2843
- isValidPort,
2844
- getVersion,
2845
- TunnelManager,
2846
- TunnelStateType,
2847
- TunnelErrorCodeType,
2848
- TunnelWarningCode,
2849
- TunnelOperations,
2850
- RemoteManagementUnauthorizedError,
2851
- buildRemoteManagementWsUrl,
2852
- parseRemoteManagement,
2853
- initiateRemoteManagement,
2854
- closeRemoteManagement,
2855
- startRemoteManagement,
2856
- getRemoteManagementState
2857
- };