pinggy 0.3.8 → 0.3.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/{chunk-65R2GMKQ.js → chunk-MBN3YBO4.js} +756 -182
- package/dist/index.cjs +860 -252
- package/dist/index.d.cts +112 -75
- package/dist/index.d.ts +112 -75
- package/dist/index.js +2 -2
- package/dist/{main-2QDG7PWL.js → main-VCUAV22W.js} +51 -32
- package/jest.config.cjs +13 -0
- package/package.json +3 -3
|
@@ -68,6 +68,9 @@ var _CLIPrinter = class _CLIPrinter {
|
|
|
68
68
|
console.error(pico2.red(pico2.bold("\u2716 Error:")), pico2.red(msg));
|
|
69
69
|
process.exit(1);
|
|
70
70
|
}
|
|
71
|
+
static red(message) {
|
|
72
|
+
return pico2.red(message);
|
|
73
|
+
}
|
|
71
74
|
static warn(message) {
|
|
72
75
|
console.warn(pico2.yellow(pico2.bold("\u26A0 Warning:")), pico2.yellow(message));
|
|
73
76
|
}
|
|
@@ -96,7 +99,7 @@ _CLIPrinter.errorDefinitions = [
|
|
|
96
99
|
message: (err) => {
|
|
97
100
|
const match = /Unknown option '(.+?)'/.exec(err.message);
|
|
98
101
|
const option = match ? match[1] : "(unknown)";
|
|
99
|
-
return `Unknown option '${option}'. Please check your command or use pinggy
|
|
102
|
+
return `Unknown option '${option}'. Please check your command or use pinggy -h for guidance.`;
|
|
100
103
|
}
|
|
101
104
|
},
|
|
102
105
|
{
|
|
@@ -121,27 +124,36 @@ var CLIPrinter = _CLIPrinter;
|
|
|
121
124
|
var printer_default = CLIPrinter;
|
|
122
125
|
|
|
123
126
|
// src/utils/util.ts
|
|
124
|
-
import {
|
|
127
|
+
import { readFileSync } from "fs";
|
|
125
128
|
import { randomUUID } from "crypto";
|
|
129
|
+
import { fileURLToPath } from "url";
|
|
130
|
+
import { dirname, join } from "path";
|
|
126
131
|
function getRandomId() {
|
|
127
132
|
return randomUUID();
|
|
128
133
|
}
|
|
129
134
|
function isValidPort(p) {
|
|
130
135
|
return Number.isInteger(p) && p > 0 && p < 65536;
|
|
131
136
|
}
|
|
132
|
-
var
|
|
133
|
-
var
|
|
137
|
+
var __filename2 = fileURLToPath(import.meta.url);
|
|
138
|
+
var __dirname2 = dirname(__filename2);
|
|
134
139
|
function getVersion() {
|
|
135
|
-
|
|
140
|
+
try {
|
|
141
|
+
const packageJsonPath = join(__dirname2, "../package.json");
|
|
142
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
143
|
+
return pkg.version ?? "";
|
|
144
|
+
} catch (error) {
|
|
145
|
+
printer_default.error("Error reading version info");
|
|
146
|
+
return "";
|
|
147
|
+
}
|
|
136
148
|
}
|
|
137
149
|
|
|
138
150
|
// src/tunnel_manager/TunnelManager.ts
|
|
139
151
|
import { pinggy } from "@pinggy/pinggy";
|
|
140
152
|
import path from "path";
|
|
141
153
|
import { Worker } from "worker_threads";
|
|
142
|
-
import { fileURLToPath } from "url";
|
|
143
|
-
var
|
|
144
|
-
var
|
|
154
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
155
|
+
var __filename3 = fileURLToPath2(import.meta.url);
|
|
156
|
+
var __dirname3 = path.dirname(__filename3);
|
|
145
157
|
var TunnelManager = class _TunnelManager {
|
|
146
158
|
constructor() {
|
|
147
159
|
this.tunnelsByTunnelId = /* @__PURE__ */ new Map();
|
|
@@ -149,6 +161,7 @@ var TunnelManager = class _TunnelManager {
|
|
|
149
161
|
this.tunnelStats = /* @__PURE__ */ new Map();
|
|
150
162
|
this.tunnelStatsListeners = /* @__PURE__ */ new Map();
|
|
151
163
|
this.tunnelErrorListeners = /* @__PURE__ */ new Map();
|
|
164
|
+
this.tunnelPollingErrorListeners = /* @__PURE__ */ new Map();
|
|
152
165
|
this.tunnelDisconnectListeners = /* @__PURE__ */ new Map();
|
|
153
166
|
this.tunnelWorkerErrorListeners = /* @__PURE__ */ new Map();
|
|
154
167
|
this.tunnelStartListeners = /* @__PURE__ */ new Map();
|
|
@@ -165,12 +178,9 @@ var TunnelManager = class _TunnelManager {
|
|
|
165
178
|
}
|
|
166
179
|
/**
|
|
167
180
|
* Creates a new managed tunnel instance with the given configuration.
|
|
168
|
-
*
|
|
181
|
+
* Optionally builds the config with forwarding rules based on buildConfig flag.
|
|
169
182
|
*
|
|
170
183
|
* @param config - The tunnel configuration options
|
|
171
|
-
* @param config.configid - Unique identifier for the tunnel configuration
|
|
172
|
-
* @param config.tunnelid - Optional custom tunnel identifier. If not provided, a random UUID will be generated
|
|
173
|
-
* @param config.additionalForwarding - Optional array of additional forwarding configurations
|
|
174
184
|
*
|
|
175
185
|
* @throws {Error} When configId is invalid or empty
|
|
176
186
|
* @throws {Error} When a tunnel with the given configId already exists
|
|
@@ -179,24 +189,19 @@ var TunnelManager = class _TunnelManager {
|
|
|
179
189
|
* status information, and statistics
|
|
180
190
|
*/
|
|
181
191
|
async createTunnel(config) {
|
|
182
|
-
const {
|
|
183
|
-
|
|
184
|
-
|
|
192
|
+
const { configId, tunnelid: requestedTunnelId, tunnelName, name, serve } = config;
|
|
193
|
+
const tunnelid = requestedTunnelId || getRandomId();
|
|
194
|
+
const autoReconnect = config.autoReconnect || false;
|
|
195
|
+
if (!configId || typeof configId !== "string" || configId.trim() === "") {
|
|
196
|
+
throw new Error("configId is required and must be a non-empty string");
|
|
185
197
|
}
|
|
186
|
-
if (this.tunnelsByConfigId.has(configid)) {
|
|
187
|
-
throw new Error(`Tunnel with configId "${configid}" already exists`);
|
|
188
|
-
}
|
|
189
|
-
const tunnelid = config.tunnelid || getRandomId();
|
|
190
|
-
const configWithForwarding = this.buildPinggyConfig(config, additionalForwarding);
|
|
191
198
|
return this._createTunnelWithProcessedConfig({
|
|
192
|
-
|
|
199
|
+
configId,
|
|
193
200
|
tunnelid,
|
|
194
|
-
tunnelName,
|
|
201
|
+
tunnelName: tunnelName || name,
|
|
195
202
|
originalConfig: config,
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
serve: config.serve,
|
|
199
|
-
autoReconnect: config.autoReconnect !== void 0 ? config.autoReconnect : false
|
|
203
|
+
serve,
|
|
204
|
+
autoReconnect
|
|
200
205
|
});
|
|
201
206
|
}
|
|
202
207
|
/**
|
|
@@ -210,7 +215,8 @@ var TunnelManager = class _TunnelManager {
|
|
|
210
215
|
async _createTunnelWithProcessedConfig(params) {
|
|
211
216
|
let instance;
|
|
212
217
|
try {
|
|
213
|
-
instance
|
|
218
|
+
logger.debug("Creating tunnel instance with processed config", params.originalConfig);
|
|
219
|
+
instance = await pinggy.createTunnel(params.originalConfig);
|
|
214
220
|
} catch (e) {
|
|
215
221
|
logger.error("Error creating tunnel instance:", e);
|
|
216
222
|
throw e;
|
|
@@ -218,25 +224,25 @@ var TunnelManager = class _TunnelManager {
|
|
|
218
224
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
219
225
|
const managed = {
|
|
220
226
|
tunnelid: params.tunnelid,
|
|
221
|
-
|
|
227
|
+
configId: params.configId,
|
|
222
228
|
tunnelName: params.tunnelName,
|
|
223
229
|
instance,
|
|
224
230
|
tunnelConfig: params.originalConfig,
|
|
225
|
-
configWithForwarding: params.configWithForwarding,
|
|
226
|
-
additionalForwarding: params.additionalForwarding,
|
|
227
231
|
serve: params.serve,
|
|
228
232
|
warnings: [],
|
|
229
233
|
isStopped: false,
|
|
230
234
|
createdAt: now,
|
|
231
235
|
startedAt: null,
|
|
232
236
|
stoppedAt: null,
|
|
233
|
-
autoReconnect: params.autoReconnect
|
|
237
|
+
autoReconnect: params.autoReconnect,
|
|
238
|
+
lastError: {}
|
|
234
239
|
};
|
|
235
240
|
instance.setTunnelEstablishedCallback(({}) => {
|
|
236
241
|
managed.startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
237
242
|
});
|
|
238
243
|
this.setupStatsCallback(params.tunnelid, managed);
|
|
239
244
|
this.setupErrorCallback(params.tunnelid, managed);
|
|
245
|
+
this.setupTunnelPollingErrorCallback(params.tunnelid, managed);
|
|
240
246
|
this.setupDisconnectCallback(params.tunnelid, managed);
|
|
241
247
|
this.setupWillReconnectCallback(params.tunnelid, managed);
|
|
242
248
|
this.setupReconnectingCallback(params.tunnelid, managed);
|
|
@@ -244,44 +250,10 @@ var TunnelManager = class _TunnelManager {
|
|
|
244
250
|
this.setupReconnectionFailedCallback(params.tunnelid, managed);
|
|
245
251
|
this.setUpTunnelWorkerErrorCallback(params.tunnelid, managed);
|
|
246
252
|
this.tunnelsByTunnelId.set(params.tunnelid, managed);
|
|
247
|
-
this.tunnelsByConfigId.set(params.
|
|
248
|
-
logger.info("Tunnel created", {
|
|
253
|
+
this.tunnelsByConfigId.set(params.configId, managed);
|
|
254
|
+
logger.info("Tunnel created", { configId: params.configId, tunnelId: params.tunnelid });
|
|
249
255
|
return managed;
|
|
250
256
|
}
|
|
251
|
-
/**
|
|
252
|
-
* Builds the Pinggy configuration by merging the default forwarding rule
|
|
253
|
-
* with additional forwarding rules from additionalForwarding array.
|
|
254
|
-
*
|
|
255
|
-
* @param config - The base Pinggy configuration
|
|
256
|
-
* @param additionalForwarding - Optional array of additional forwarding rules
|
|
257
|
-
* @returns Modified PinggyOptions
|
|
258
|
-
*/
|
|
259
|
-
buildPinggyConfig(config, additionalForwarding) {
|
|
260
|
-
const forwardingRules = [];
|
|
261
|
-
if (config.forwarding) {
|
|
262
|
-
forwardingRules.push({
|
|
263
|
-
type: config.tunnelType && config.tunnelType[0] || "http",
|
|
264
|
-
address: config.forwarding
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
if (Array.isArray(additionalForwarding) && additionalForwarding.length > 0) {
|
|
268
|
-
for (const rule of additionalForwarding) {
|
|
269
|
-
if (rule && rule.localDomain && rule.localPort && rule.remoteDomain && isValidPort(rule.localPort)) {
|
|
270
|
-
const forwardingRule = {
|
|
271
|
-
type: rule.protocol,
|
|
272
|
-
// In Future we can make this dynamic based on user input
|
|
273
|
-
address: `${rule.localDomain}:${rule.localPort}`,
|
|
274
|
-
listenAddress: rule.remotePort && isValidPort(rule.remotePort) ? `${rule.remoteDomain}:${rule.remotePort}` : rule.remoteDomain
|
|
275
|
-
};
|
|
276
|
-
forwardingRules.push(forwardingRule);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
return {
|
|
281
|
-
...config,
|
|
282
|
-
forwarding: forwardingRules.length > 0 ? forwardingRules : config.forwarding
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
257
|
/**
|
|
286
258
|
* Start a tunnel that was created but not yet started
|
|
287
259
|
*/
|
|
@@ -293,7 +265,14 @@ var TunnelManager = class _TunnelManager {
|
|
|
293
265
|
try {
|
|
294
266
|
urls = await managed.instance.start();
|
|
295
267
|
} catch (error) {
|
|
296
|
-
logger.
|
|
268
|
+
logger.warn("Failed to start tunnel", { tunnelId, error });
|
|
269
|
+
managed.isStopped = true;
|
|
270
|
+
managed.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
271
|
+
managed.lastError = {
|
|
272
|
+
message: "Failed to start tunnel",
|
|
273
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
274
|
+
isFatal: true
|
|
275
|
+
};
|
|
297
276
|
throw error;
|
|
298
277
|
}
|
|
299
278
|
logger.info("Tunnel started", { tunnelId, urls });
|
|
@@ -329,7 +308,7 @@ var TunnelManager = class _TunnelManager {
|
|
|
329
308
|
stopTunnel(tunnelId) {
|
|
330
309
|
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
331
310
|
if (!managed) throw new Error(`Tunnel "${tunnelId}" not found`);
|
|
332
|
-
logger.info("Stopping tunnel", { tunnelId, configId: managed.
|
|
311
|
+
logger.info("Stopping tunnel", { tunnelId, configId: managed.configId });
|
|
333
312
|
try {
|
|
334
313
|
managed.instance.stop();
|
|
335
314
|
if (managed.serveWorker) {
|
|
@@ -341,6 +320,7 @@ var TunnelManager = class _TunnelManager {
|
|
|
341
320
|
this.tunnelStats.delete(tunnelId);
|
|
342
321
|
this.tunnelStatsListeners.delete(tunnelId);
|
|
343
322
|
this.tunnelErrorListeners.delete(tunnelId);
|
|
323
|
+
this.tunnelPollingErrorListeners.delete(tunnelId);
|
|
344
324
|
this.tunnelDisconnectListeners.delete(tunnelId);
|
|
345
325
|
this.tunnelWorkerErrorListeners.delete(tunnelId);
|
|
346
326
|
this.tunnelStartListeners.delete(tunnelId);
|
|
@@ -352,8 +332,8 @@ var TunnelManager = class _TunnelManager {
|
|
|
352
332
|
managed.warnings = managed.warnings ?? [];
|
|
353
333
|
managed.isStopped = true;
|
|
354
334
|
managed.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
355
|
-
logger.info("Tunnel stopped", { tunnelId, configId: managed.
|
|
356
|
-
return {
|
|
335
|
+
logger.info("Tunnel stopped", { tunnelId, configId: managed.configId });
|
|
336
|
+
return { configId: managed.configId, tunnelid: managed.tunnelid };
|
|
357
337
|
} catch (error) {
|
|
358
338
|
logger.error("Failed to stop tunnel", { tunnelId, error });
|
|
359
339
|
throw error;
|
|
@@ -370,7 +350,6 @@ var TunnelManager = class _TunnelManager {
|
|
|
370
350
|
return [];
|
|
371
351
|
}
|
|
372
352
|
const urls = await managed.instance.urls();
|
|
373
|
-
logger.debug("Queried tunnel URLs", { tunnelId, urls });
|
|
374
353
|
return urls;
|
|
375
354
|
} catch (error) {
|
|
376
355
|
logger.error("Error fetching tunnel URLs", { tunnelId, error });
|
|
@@ -386,11 +365,10 @@ var TunnelManager = class _TunnelManager {
|
|
|
386
365
|
const tunnelList = await Promise.all(Array.from(this.tunnelsByTunnelId.values()).map(async (tunnel) => {
|
|
387
366
|
return {
|
|
388
367
|
tunnelid: tunnel.tunnelid,
|
|
389
|
-
|
|
368
|
+
configId: tunnel.configId,
|
|
390
369
|
tunnelName: tunnel.tunnelName,
|
|
391
370
|
tunnelConfig: tunnel.tunnelConfig,
|
|
392
|
-
remoteurls:
|
|
393
|
-
additionalForwarding: tunnel.additionalForwarding,
|
|
371
|
+
remoteurls: tunnel.isStopped || tunnel.lastError?.isFatal ? [] : await this.getTunnelUrls(tunnel.tunnelid),
|
|
394
372
|
serve: tunnel.serve
|
|
395
373
|
};
|
|
396
374
|
}));
|
|
@@ -413,7 +391,6 @@ var TunnelManager = class _TunnelManager {
|
|
|
413
391
|
return "exited";
|
|
414
392
|
}
|
|
415
393
|
const status = await managed.instance.getStatus();
|
|
416
|
-
logger.debug("Queried tunnel status", { tunnelId, status });
|
|
417
394
|
return status;
|
|
418
395
|
}
|
|
419
396
|
/**
|
|
@@ -431,6 +408,15 @@ var TunnelManager = class _TunnelManager {
|
|
|
431
408
|
this.tunnelsByConfigId.clear();
|
|
432
409
|
this.tunnelStats.clear();
|
|
433
410
|
this.tunnelStatsListeners.clear();
|
|
411
|
+
this.tunnelErrorListeners.clear();
|
|
412
|
+
this.tunnelPollingErrorListeners.clear();
|
|
413
|
+
this.tunnelDisconnectListeners.clear();
|
|
414
|
+
this.tunnelWorkerErrorListeners.clear();
|
|
415
|
+
this.tunnelStartListeners.clear();
|
|
416
|
+
this.tunnelWillReconnectListeners.clear();
|
|
417
|
+
this.tunnelReconnectingListeners.clear();
|
|
418
|
+
this.tunnelReconnectionCompletedListeners.clear();
|
|
419
|
+
this.tunnelReconnectionFailedListeners.clear();
|
|
434
420
|
logger.info("All tunnels stopped and cleared");
|
|
435
421
|
}
|
|
436
422
|
/**
|
|
@@ -451,7 +437,7 @@ var TunnelManager = class _TunnelManager {
|
|
|
451
437
|
return false;
|
|
452
438
|
}
|
|
453
439
|
this._cleanupTunnelRecords(managed);
|
|
454
|
-
logger.info("Removed stopped tunnel records", { tunnelId, configId: managed.
|
|
440
|
+
logger.info("Removed stopped tunnel records", { tunnelId, configId: managed.configId });
|
|
455
441
|
return true;
|
|
456
442
|
}
|
|
457
443
|
/**
|
|
@@ -478,6 +464,7 @@ var TunnelManager = class _TunnelManager {
|
|
|
478
464
|
this.tunnelStats.delete(managed.tunnelid);
|
|
479
465
|
this.tunnelStatsListeners.delete(managed.tunnelid);
|
|
480
466
|
this.tunnelErrorListeners.delete(managed.tunnelid);
|
|
467
|
+
this.tunnelPollingErrorListeners.delete(managed.tunnelid);
|
|
481
468
|
this.tunnelDisconnectListeners.delete(managed.tunnelid);
|
|
482
469
|
this.tunnelWorkerErrorListeners.delete(managed.tunnelid);
|
|
483
470
|
this.tunnelStartListeners.delete(managed.tunnelid);
|
|
@@ -486,7 +473,7 @@ var TunnelManager = class _TunnelManager {
|
|
|
486
473
|
this.tunnelReconnectionCompletedListeners.delete(managed.tunnelid);
|
|
487
474
|
this.tunnelReconnectionFailedListeners.delete(managed.tunnelid);
|
|
488
475
|
this.tunnelsByTunnelId.delete(managed.tunnelid);
|
|
489
|
-
this.tunnelsByConfigId.delete(managed.
|
|
476
|
+
this.tunnelsByConfigId.delete(managed.configId);
|
|
490
477
|
} catch (e) {
|
|
491
478
|
logger.warn("Failed cleaning up tunnel records", { tunnelId: managed.tunnelid, error: e });
|
|
492
479
|
}
|
|
@@ -547,21 +534,20 @@ var TunnelManager = class _TunnelManager {
|
|
|
547
534
|
}
|
|
548
535
|
logger.info("Initiating tunnel restart", {
|
|
549
536
|
tunnelId: tunnelid,
|
|
550
|
-
configId: existingTunnel.
|
|
537
|
+
configId: existingTunnel.configId
|
|
551
538
|
});
|
|
552
539
|
try {
|
|
553
540
|
const tunnelName = existingTunnel.tunnelName;
|
|
554
|
-
const currentConfigId = existingTunnel.
|
|
541
|
+
const currentConfigId = existingTunnel.configId;
|
|
555
542
|
const currentConfig = existingTunnel.tunnelConfig;
|
|
556
|
-
const configWithForwarding = existingTunnel.configWithForwarding;
|
|
557
|
-
const additionalForwarding = existingTunnel.additionalForwarding;
|
|
558
543
|
const currentServe = existingTunnel.serve;
|
|
559
544
|
const autoReconnect = existingTunnel.autoReconnect || false;
|
|
560
545
|
this.tunnelsByTunnelId.delete(tunnelid);
|
|
561
|
-
this.tunnelsByConfigId.delete(existingTunnel.
|
|
546
|
+
this.tunnelsByConfigId.delete(existingTunnel.configId);
|
|
562
547
|
this.tunnelStats.delete(tunnelid);
|
|
563
548
|
this.tunnelStatsListeners.delete(tunnelid);
|
|
564
549
|
this.tunnelErrorListeners.delete(tunnelid);
|
|
550
|
+
this.tunnelPollingErrorListeners.delete(tunnelid);
|
|
565
551
|
this.tunnelDisconnectListeners.delete(tunnelid);
|
|
566
552
|
this.tunnelWorkerErrorListeners.delete(tunnelid);
|
|
567
553
|
this.tunnelStartListeners.delete(tunnelid);
|
|
@@ -570,12 +556,10 @@ var TunnelManager = class _TunnelManager {
|
|
|
570
556
|
this.tunnelReconnectionCompletedListeners.delete(tunnelid);
|
|
571
557
|
this.tunnelReconnectionFailedListeners.delete(tunnelid);
|
|
572
558
|
const newTunnel = await this._createTunnelWithProcessedConfig({
|
|
573
|
-
|
|
559
|
+
configId: currentConfigId,
|
|
574
560
|
tunnelid,
|
|
575
561
|
tunnelName,
|
|
576
562
|
originalConfig: currentConfig,
|
|
577
|
-
configWithForwarding,
|
|
578
|
-
additionalForwarding,
|
|
579
563
|
serve: currentServe,
|
|
580
564
|
autoReconnect
|
|
581
565
|
});
|
|
@@ -604,20 +588,18 @@ var TunnelManager = class _TunnelManager {
|
|
|
604
588
|
* @throws Error if the tunnel is not found or if the update process fails
|
|
605
589
|
*/
|
|
606
590
|
async updateConfig(newConfig) {
|
|
607
|
-
const {
|
|
608
|
-
if (!
|
|
609
|
-
throw new Error(`Invalid
|
|
591
|
+
const { configId, tunnelName: newTunnelName } = newConfig;
|
|
592
|
+
if (!configId || configId.trim().length === 0) {
|
|
593
|
+
throw new Error(`Invalid configId: "${configId}"`);
|
|
610
594
|
}
|
|
611
|
-
const existingTunnel = this.tunnelsByConfigId.get(
|
|
595
|
+
const existingTunnel = this.tunnelsByConfigId.get(configId);
|
|
612
596
|
if (!existingTunnel) {
|
|
613
|
-
throw new Error(`Tunnel with config id "${
|
|
597
|
+
throw new Error(`Tunnel with config id "${configId}" not found`);
|
|
614
598
|
}
|
|
615
599
|
const isStopped = existingTunnel.isStopped;
|
|
616
600
|
const currentTunnelConfig = existingTunnel.tunnelConfig;
|
|
617
|
-
const currentConfigWithForwarding = existingTunnel.configWithForwarding;
|
|
618
601
|
const currentTunnelId = existingTunnel.tunnelid;
|
|
619
|
-
const currentTunnelConfigId = existingTunnel.
|
|
620
|
-
const currentAdditionalForwarding = existingTunnel.additionalForwarding;
|
|
602
|
+
const currentTunnelConfigId = existingTunnel.configId;
|
|
621
603
|
const currentTunnelName = existingTunnel.tunnelName;
|
|
622
604
|
const currentServe = existingTunnel.serve;
|
|
623
605
|
const currentAutoReconnect = existingTunnel.autoReconnect || false;
|
|
@@ -629,22 +611,19 @@ var TunnelManager = class _TunnelManager {
|
|
|
629
611
|
this.tunnelsByConfigId.delete(currentTunnelConfigId);
|
|
630
612
|
const mergedBaseConfig = {
|
|
631
613
|
...newConfig,
|
|
632
|
-
|
|
614
|
+
configId,
|
|
633
615
|
tunnelName: newTunnelName !== void 0 ? newTunnelName : currentTunnelName,
|
|
634
616
|
serve: newConfig.serve !== void 0 ? newConfig.serve : currentServe
|
|
635
617
|
};
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
);
|
|
618
|
+
const effectiveServe = newConfig.serve !== void 0 ? newConfig.serve : currentServe;
|
|
619
|
+
const effectiveTunnelName = newTunnelName !== void 0 ? newTunnelName : currentTunnelName;
|
|
620
|
+
let configWithForwarding;
|
|
640
621
|
const newTunnel = await this._createTunnelWithProcessedConfig({
|
|
641
|
-
|
|
622
|
+
configId,
|
|
642
623
|
tunnelid: currentTunnelId,
|
|
643
|
-
tunnelName:
|
|
624
|
+
tunnelName: effectiveTunnelName,
|
|
644
625
|
originalConfig: mergedBaseConfig,
|
|
645
|
-
|
|
646
|
-
additionalForwarding: additionalForwarding !== void 0 ? additionalForwarding : currentAdditionalForwarding,
|
|
647
|
-
serve: newConfig.serve !== void 0 ? newConfig.serve : currentServe,
|
|
626
|
+
serve: effectiveServe,
|
|
648
627
|
autoReconnect: currentAutoReconnect
|
|
649
628
|
});
|
|
650
629
|
if (!isStopped) {
|
|
@@ -652,23 +631,21 @@ var TunnelManager = class _TunnelManager {
|
|
|
652
631
|
}
|
|
653
632
|
logger.info("Tunnel configuration updated", {
|
|
654
633
|
tunnelId: newTunnel.tunnelid,
|
|
655
|
-
configId: newTunnel.
|
|
634
|
+
configId: newTunnel.configId,
|
|
656
635
|
isStopped
|
|
657
636
|
});
|
|
658
637
|
return newTunnel;
|
|
659
638
|
} catch (error) {
|
|
660
639
|
logger.error("Error updating tunnel configuration", {
|
|
661
|
-
configId
|
|
640
|
+
configId,
|
|
662
641
|
error: error instanceof Error ? error.message : String(error)
|
|
663
642
|
});
|
|
664
643
|
try {
|
|
665
644
|
const originalTunnel = await this._createTunnelWithProcessedConfig({
|
|
666
|
-
|
|
645
|
+
configId: currentTunnelConfigId,
|
|
667
646
|
tunnelid: currentTunnelId,
|
|
668
647
|
tunnelName: currentTunnelName,
|
|
669
648
|
originalConfig: currentTunnelConfig,
|
|
670
|
-
configWithForwarding: currentConfigWithForwarding,
|
|
671
|
-
additionalForwarding: currentAdditionalForwarding,
|
|
672
649
|
serve: currentServe,
|
|
673
650
|
autoReconnect: currentAutoReconnect
|
|
674
651
|
});
|
|
@@ -784,6 +761,19 @@ var TunnelManager = class _TunnelManager {
|
|
|
784
761
|
logger.info("Error listener registered for tunnel", { tunnelId, listenerId });
|
|
785
762
|
return listenerId;
|
|
786
763
|
}
|
|
764
|
+
async registerPollingErrorListener(tunnelId, listener) {
|
|
765
|
+
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
766
|
+
if (!managed) {
|
|
767
|
+
throw new Error(`Tunnel "${tunnelId}" not found`);
|
|
768
|
+
}
|
|
769
|
+
if (!this.tunnelPollingErrorListeners.has(tunnelId)) {
|
|
770
|
+
this.tunnelPollingErrorListeners.set(tunnelId, /* @__PURE__ */ new Map());
|
|
771
|
+
}
|
|
772
|
+
const listenerId = getRandomId();
|
|
773
|
+
this.tunnelPollingErrorListeners.get(tunnelId).set(listenerId, listener);
|
|
774
|
+
logger.info("Polling error listener registered for tunnel", { tunnelId, listenerId });
|
|
775
|
+
return listenerId;
|
|
776
|
+
}
|
|
787
777
|
async registerDisconnectListener(tunnelId, listener) {
|
|
788
778
|
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
789
779
|
if (!managed) {
|
|
@@ -915,6 +905,22 @@ var TunnelManager = class _TunnelManager {
|
|
|
915
905
|
logger.warn("Attempted to deregister non-existent error listener", { tunnelId, listenerId });
|
|
916
906
|
}
|
|
917
907
|
}
|
|
908
|
+
deregisterPollingErrorListener(tunnelId, listenerId) {
|
|
909
|
+
const listeners = this.tunnelPollingErrorListeners.get(tunnelId);
|
|
910
|
+
if (!listeners) {
|
|
911
|
+
logger.warn("No polling error listeners found for tunnel", { tunnelId });
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
const removed = listeners.delete(listenerId);
|
|
915
|
+
if (removed) {
|
|
916
|
+
logger.info("Polling error listener deregistered", { tunnelId, listenerId });
|
|
917
|
+
if (listeners.size === 0) {
|
|
918
|
+
this.tunnelPollingErrorListeners.delete(tunnelId);
|
|
919
|
+
}
|
|
920
|
+
} else {
|
|
921
|
+
logger.warn("Attempted to deregister non-existent polling error listener", { tunnelId, listenerId });
|
|
922
|
+
}
|
|
923
|
+
}
|
|
918
924
|
deregisterDisconnectListener(tunnelId, listenerId) {
|
|
919
925
|
const listeners = this.tunnelDisconnectListeners.get(tunnelId);
|
|
920
926
|
if (!listeners) {
|
|
@@ -1032,6 +1038,46 @@ var TunnelManager = class _TunnelManager {
|
|
|
1032
1038
|
logger.warn("Failed to set up stats callback", { tunnelId, error });
|
|
1033
1039
|
}
|
|
1034
1040
|
}
|
|
1041
|
+
setupTunnelPollingErrorCallback(tunnelId, managed) {
|
|
1042
|
+
try {
|
|
1043
|
+
const callback = ({ error }) => {
|
|
1044
|
+
try {
|
|
1045
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1046
|
+
logger.info("Tunnel reported polling error", { tunnelId, errorMessage });
|
|
1047
|
+
const managedTunnel = this.tunnelsByTunnelId.get(tunnelId);
|
|
1048
|
+
if (managedTunnel) {
|
|
1049
|
+
managedTunnel.lastError = {
|
|
1050
|
+
message: errorMessage,
|
|
1051
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1052
|
+
isFatal: true
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
this.notifyPollingErrorListeners(tunnelId, errorMessage);
|
|
1056
|
+
} catch (e) {
|
|
1057
|
+
logger.warn("Error handling tunnel polling error callback", { tunnelId, e });
|
|
1058
|
+
}
|
|
1059
|
+
};
|
|
1060
|
+
managed.instance.setPollingErrorCallback(callback);
|
|
1061
|
+
logger.debug("Tunnel polling error callback set up for tunnel", { tunnelId });
|
|
1062
|
+
} catch (error) {
|
|
1063
|
+
logger.warn("Failed to set up tunnel polling error callback", { tunnelId, error });
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
notifyPollingErrorListeners(tunnelId, errorMsg) {
|
|
1067
|
+
try {
|
|
1068
|
+
const listeners = this.tunnelPollingErrorListeners.get(tunnelId);
|
|
1069
|
+
if (!listeners) return;
|
|
1070
|
+
for (const [id, listener] of listeners) {
|
|
1071
|
+
try {
|
|
1072
|
+
listener(tunnelId, errorMsg);
|
|
1073
|
+
} catch (err) {
|
|
1074
|
+
logger.debug("Error in polling-error-listener callback", { listenerId: id, tunnelId, err });
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
} catch (err) {
|
|
1078
|
+
logger.debug("Failed to notify polling error listeners", { tunnelId, err });
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1035
1081
|
notifyErrorListeners(tunnelId, errorMsg, isFatal) {
|
|
1036
1082
|
try {
|
|
1037
1083
|
const listeners = this.tunnelErrorListeners.get(tunnelId);
|
|
@@ -1054,6 +1100,14 @@ var TunnelManager = class _TunnelManager {
|
|
|
1054
1100
|
const msg = typeof error === "string" ? error : String(error);
|
|
1055
1101
|
const isFatal = true;
|
|
1056
1102
|
logger.debug("Tunnel reported error", { tunnelId, errorNo, errorMsg: msg, recoverable });
|
|
1103
|
+
const managedTunnel = this.tunnelsByTunnelId.get(tunnelId);
|
|
1104
|
+
if (managedTunnel) {
|
|
1105
|
+
managedTunnel.lastError = {
|
|
1106
|
+
message: msg,
|
|
1107
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1108
|
+
isFatal: false
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1057
1111
|
this.notifyErrorListeners(tunnelId, msg, isFatal);
|
|
1058
1112
|
} catch (e) {
|
|
1059
1113
|
logger.warn("Error handling tunnel error callback", { tunnelId, e });
|
|
@@ -1304,9 +1358,9 @@ var TunnelManager = class _TunnelManager {
|
|
|
1304
1358
|
}
|
|
1305
1359
|
startStaticFileServer(managed) {
|
|
1306
1360
|
try {
|
|
1307
|
-
const
|
|
1308
|
-
const
|
|
1309
|
-
const fileServerWorkerPath = path.join(
|
|
1361
|
+
const __filename4 = fileURLToPath2(import.meta.url);
|
|
1362
|
+
const __dirname4 = path.dirname(__filename4);
|
|
1363
|
+
const fileServerWorkerPath = path.join(__dirname4, "workers", "file_serve_worker.cjs");
|
|
1310
1364
|
const staticServerWorker = new Worker(fileServerWorkerPath, {
|
|
1311
1365
|
workerData: {
|
|
1312
1366
|
dir: managed.serve,
|
|
@@ -1449,11 +1503,11 @@ var RemoteManagementStatus = {
|
|
|
1449
1503
|
};
|
|
1450
1504
|
|
|
1451
1505
|
// src/remote_management/remote_schema.ts
|
|
1452
|
-
import { TunnelType
|
|
1506
|
+
import { TunnelType } from "@pinggy/pinggy";
|
|
1453
1507
|
import { z } from "zod";
|
|
1454
1508
|
var HeaderModificationSchema = z.object({
|
|
1455
1509
|
key: z.string(),
|
|
1456
|
-
value: z.array(z.string()).optional(),
|
|
1510
|
+
value: z.array(z.string()).nullable().optional(),
|
|
1457
1511
|
type: z.enum(["add", "remove", "update"])
|
|
1458
1512
|
});
|
|
1459
1513
|
var AdditionalForwardingSchema = z.object({
|
|
@@ -1469,7 +1523,7 @@ var TunnelConfigSchema = z.object({
|
|
|
1469
1523
|
// legacy key
|
|
1470
1524
|
autoreconnect: z.boolean(),
|
|
1471
1525
|
basicauth: z.array(z.object({ username: z.string(), password: z.string() })).nullable(),
|
|
1472
|
-
bearerauth: z.string().nullable(),
|
|
1526
|
+
bearerauth: z.array(z.string()).nullable(),
|
|
1473
1527
|
configid: z.string(),
|
|
1474
1528
|
configname: z.string(),
|
|
1475
1529
|
greetmsg: z.string().optional(),
|
|
@@ -1491,11 +1545,11 @@ var TunnelConfigSchema = z.object({
|
|
|
1491
1545
|
token: z.string(),
|
|
1492
1546
|
tunnelTimeout: z.number(),
|
|
1493
1547
|
type: z.enum([
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1548
|
+
TunnelType.Http,
|
|
1549
|
+
TunnelType.Tcp,
|
|
1550
|
+
TunnelType.Udp,
|
|
1551
|
+
TunnelType.Tls,
|
|
1552
|
+
TunnelType.TlsTcp
|
|
1499
1553
|
]),
|
|
1500
1554
|
webdebuggerport: z.number(),
|
|
1501
1555
|
xff: z.string(),
|
|
@@ -1526,15 +1580,102 @@ var RestartSchema = StopSchema;
|
|
|
1526
1580
|
var UpdateConfigSchema = z.object({
|
|
1527
1581
|
tunnelConfig: TunnelConfigSchema
|
|
1528
1582
|
});
|
|
1583
|
+
var ForwardingEntryV2Schema = z.object({
|
|
1584
|
+
listenAddress: z.string().optional(),
|
|
1585
|
+
address: z.string(),
|
|
1586
|
+
type: z.enum([TunnelType.Http, TunnelType.Tcp, TunnelType.Udp, TunnelType.Tls, TunnelType.TlsTcp]).optional()
|
|
1587
|
+
});
|
|
1588
|
+
var TunnelConfigV1Schema = z.object({
|
|
1589
|
+
// Meta Info
|
|
1590
|
+
version: z.string(),
|
|
1591
|
+
name: z.string(),
|
|
1592
|
+
configId: z.string(),
|
|
1593
|
+
// General tunnel configurations
|
|
1594
|
+
serverAddress: z.string().optional(),
|
|
1595
|
+
token: z.string().optional(),
|
|
1596
|
+
autoReconnect: z.boolean().optional(),
|
|
1597
|
+
reconnectInterval: z.number().optional(),
|
|
1598
|
+
maxReconnectAttempts: z.number().optional(),
|
|
1599
|
+
force: z.boolean(),
|
|
1600
|
+
keepAliveInterval: z.number().optional(),
|
|
1601
|
+
webDebugger: z.string(),
|
|
1602
|
+
//Forwarding
|
|
1603
|
+
// Either a URL string (e.g. "https://localhost:5555") or an array of forwarding entries.
|
|
1604
|
+
forwarding: z.union([
|
|
1605
|
+
z.string(),
|
|
1606
|
+
z.array(ForwardingEntryV2Schema)
|
|
1607
|
+
]),
|
|
1608
|
+
// IP whitelist
|
|
1609
|
+
ipWhitelist: z.array(z.string()).optional(),
|
|
1610
|
+
basicAuth: z.array(z.object({ username: z.string(), password: z.string() })).optional(),
|
|
1611
|
+
bearerTokenAuth: z.array(z.string()).optional(),
|
|
1612
|
+
headerModification: z.array(HeaderModificationSchema).optional(),
|
|
1613
|
+
reverseProxy: z.boolean().optional(),
|
|
1614
|
+
xForwardedFor: z.boolean().optional(),
|
|
1615
|
+
httpsOnly: z.boolean().optional(),
|
|
1616
|
+
originalRequestUrl: z.boolean().optional(),
|
|
1617
|
+
allowPreflight: z.boolean().optional(),
|
|
1618
|
+
serve: z.string().optional(),
|
|
1619
|
+
optional: z.record(z.string(), z.unknown()).optional()
|
|
1620
|
+
});
|
|
1621
|
+
var StartV2Schema = z.object({
|
|
1622
|
+
tunnelID: z.string().nullable().optional(),
|
|
1623
|
+
tunnelConfig: TunnelConfigV1Schema
|
|
1624
|
+
});
|
|
1625
|
+
var UpdateConfigV2Schema = z.object({
|
|
1626
|
+
tunnelConfig: TunnelConfigV1Schema
|
|
1627
|
+
});
|
|
1628
|
+
function pinggyOptionsToTunnelConfigV1(opts, configStoredInCli) {
|
|
1629
|
+
const parsedTokens = opts.bearerTokenAuth ? Array.isArray(opts.bearerTokenAuth) ? opts.bearerTokenAuth : JSON.parse(opts.bearerTokenAuth) : [];
|
|
1630
|
+
return {
|
|
1631
|
+
version: configStoredInCli.version || "1.0",
|
|
1632
|
+
name: configStoredInCli.name || "",
|
|
1633
|
+
configId: configStoredInCli.configId || "",
|
|
1634
|
+
serverAddress: opts.serverAddress || "a.pinggy.io:443",
|
|
1635
|
+
token: opts.token || "",
|
|
1636
|
+
autoReconnect: opts.autoReconnect ?? true,
|
|
1637
|
+
force: opts.force ?? false,
|
|
1638
|
+
webDebugger: opts.webDebugger || "",
|
|
1639
|
+
forwarding: opts.forwarding ? opts.forwarding : "",
|
|
1640
|
+
ipWhitelist: opts.ipWhitelist ? Array.isArray(opts.ipWhitelist) ? opts.ipWhitelist : JSON.parse(opts.ipWhitelist) : [],
|
|
1641
|
+
basicAuth: opts.basicAuth && Object.keys(opts.basicAuth).length ? opts.basicAuth : void 0,
|
|
1642
|
+
bearerTokenAuth: parsedTokens.length ? parsedTokens : void 0,
|
|
1643
|
+
headerModification: opts.headerModification || [],
|
|
1644
|
+
reverseProxy: opts.reverseProxy ?? false,
|
|
1645
|
+
xForwardedFor: !!opts.xForwardedFor,
|
|
1646
|
+
httpsOnly: opts.httpsOnly ?? false,
|
|
1647
|
+
originalRequestUrl: opts.originalRequestUrl ?? false,
|
|
1648
|
+
allowPreflight: opts.allowPreflight ?? false,
|
|
1649
|
+
optional: opts.optional || {}
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1529
1652
|
function tunnelConfigToPinggyOptions(config) {
|
|
1653
|
+
const forwardingData = [];
|
|
1654
|
+
forwardingData.push({
|
|
1655
|
+
address: `${config.forwardedhost}:${config.localport}`,
|
|
1656
|
+
type: config.type || TunnelType.Http
|
|
1657
|
+
// Default to HTTP for the primary forwarding entry
|
|
1658
|
+
});
|
|
1659
|
+
if (config.additionalForwarding && Array.isArray(config.additionalForwarding)) {
|
|
1660
|
+
config.additionalForwarding.forEach((entry) => {
|
|
1661
|
+
if (entry.localDomain && entry.localPort && entry.remoteDomain) {
|
|
1662
|
+
const listenAddress = entry.remotePort && isValidPort(entry.remotePort) ? `${entry.remoteDomain}:${entry.remotePort}` : entry.remoteDomain;
|
|
1663
|
+
forwardingData.push({
|
|
1664
|
+
address: `${entry.localDomain}:${entry.localPort}`,
|
|
1665
|
+
listenAddress,
|
|
1666
|
+
type: TunnelType.Http
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1530
1671
|
return {
|
|
1531
1672
|
token: config.token || "",
|
|
1532
1673
|
serverAddress: config.serveraddress || "free.pinggy.io",
|
|
1533
|
-
forwarding:
|
|
1674
|
+
forwarding: forwardingData,
|
|
1534
1675
|
webDebugger: config.webdebuggerport ? `localhost:${config.webdebuggerport}` : "",
|
|
1535
1676
|
ipWhitelist: config.ipwhitelist || [],
|
|
1536
1677
|
basicAuth: config.basicauth ? config.basicauth : [],
|
|
1537
|
-
bearerTokenAuth: config.bearerauth
|
|
1678
|
+
bearerTokenAuth: config.bearerauth || [],
|
|
1538
1679
|
headerModification: config.headermodification,
|
|
1539
1680
|
xForwardedFor: !!config.xff,
|
|
1540
1681
|
httpsOnly: config.httpsOnly,
|
|
@@ -1548,18 +1689,38 @@ function tunnelConfigToPinggyOptions(config) {
|
|
|
1548
1689
|
}
|
|
1549
1690
|
};
|
|
1550
1691
|
}
|
|
1551
|
-
function pinggyOptionsToTunnelConfig(opts, configid, configName, localserverTls, greetMsg,
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1692
|
+
function pinggyOptionsToTunnelConfig(opts, configid, configName, localserverTls, greetMsg, serve) {
|
|
1693
|
+
let primaryEntry;
|
|
1694
|
+
let additionalEntries = [];
|
|
1695
|
+
if (Array.isArray(opts.forwarding)) {
|
|
1696
|
+
primaryEntry = opts.forwarding.find((e) => !e.listenAddress) ?? opts.forwarding[0];
|
|
1697
|
+
additionalEntries = opts.forwarding.filter(
|
|
1698
|
+
(e) => e !== primaryEntry && Boolean(e.listenAddress)
|
|
1699
|
+
);
|
|
1700
|
+
}
|
|
1701
|
+
const forwarding = primaryEntry ? String(primaryEntry.address) : String(opts.forwarding);
|
|
1702
|
+
const [parsedForwardedHost, portStr] = forwarding.split(":");
|
|
1703
|
+
const parsedLocalPort = parseInt(portStr, 10);
|
|
1704
|
+
const tunnelType = primaryEntry?.type ?? TunnelType.Http;
|
|
1705
|
+
const additionalForwarding = additionalEntries.map((e) => {
|
|
1706
|
+
const [localDomain, localPortStr] = String(e.address).split(":");
|
|
1707
|
+
const [remoteDomain, remotePortStr] = String(e.listenAddress).split(":");
|
|
1708
|
+
const localPort = parseInt(localPortStr, 10);
|
|
1709
|
+
const remotePort = parseInt(remotePortStr, 10);
|
|
1710
|
+
return {
|
|
1711
|
+
localDomain,
|
|
1712
|
+
localPort: isNaN(localPort) ? 0 : localPort,
|
|
1713
|
+
remoteDomain,
|
|
1714
|
+
remotePort: isNaN(remotePort) ? 0 : remotePort
|
|
1715
|
+
};
|
|
1716
|
+
});
|
|
1556
1717
|
const parsedTokens = opts.bearerTokenAuth ? Array.isArray(opts.bearerTokenAuth) ? opts.bearerTokenAuth : JSON.parse(opts.bearerTokenAuth) : [];
|
|
1557
1718
|
return {
|
|
1558
1719
|
allowPreflight: opts.allowPreflight ?? false,
|
|
1559
1720
|
allowpreflight: opts.allowPreflight ?? false,
|
|
1560
1721
|
autoreconnect: opts.autoReconnect ?? false,
|
|
1561
1722
|
basicauth: opts.basicAuth && Object.keys(opts.basicAuth).length ? opts.basicAuth : null,
|
|
1562
|
-
bearerauth: parsedTokens.length ? parsedTokens.join(",") : null,
|
|
1723
|
+
bearerauth: parsedTokens.length ? [parsedTokens.join(",")] : null,
|
|
1563
1724
|
configid,
|
|
1564
1725
|
configname: configName,
|
|
1565
1726
|
greetmsg: greetMsg || "",
|
|
@@ -1590,7 +1751,6 @@ function pinggyOptionsToTunnelConfig(opts, configid, configName, localserverTls,
|
|
|
1590
1751
|
}
|
|
1591
1752
|
|
|
1592
1753
|
// src/remote_management/handler.ts
|
|
1593
|
-
import { TunnelType as TunnelType3 } from "@pinggy/pinggy";
|
|
1594
1754
|
var TunnelOperations = class {
|
|
1595
1755
|
constructor() {
|
|
1596
1756
|
this.tunnelManager = TunnelManager.getInstance();
|
|
@@ -1604,12 +1764,15 @@ var TunnelOperations = class {
|
|
|
1604
1764
|
status.starttimestamp = managed.startedAt || "";
|
|
1605
1765
|
status.endtimestamp = managed.stoppedAt || "";
|
|
1606
1766
|
}
|
|
1767
|
+
if (managed?.lastError) {
|
|
1768
|
+
status.lastError = managed.lastError;
|
|
1769
|
+
}
|
|
1607
1770
|
} catch (e) {
|
|
1608
1771
|
}
|
|
1609
1772
|
return status;
|
|
1610
1773
|
}
|
|
1611
1774
|
// --- Helper to construct TunnelResponse ---
|
|
1612
|
-
async buildTunnelResponse(tunnelid, tunnelConfig, configid, tunnelName,
|
|
1775
|
+
async buildTunnelResponse(tunnelid, tunnelConfig, configid, tunnelName, serve) {
|
|
1613
1776
|
const [status, stats, tlsInfo, greetMsg, remoteurls] = await Promise.all([
|
|
1614
1777
|
this.tunnelManager.getTunnelStatus(tunnelid),
|
|
1615
1778
|
this.tunnelManager.getLatestTunnelStats(tunnelid) || newStats(),
|
|
@@ -1620,11 +1783,27 @@ var TunnelOperations = class {
|
|
|
1620
1783
|
return {
|
|
1621
1784
|
tunnelid,
|
|
1622
1785
|
remoteurls,
|
|
1623
|
-
tunnelconfig: pinggyOptionsToTunnelConfig(tunnelConfig, configid, tunnelName, tlsInfo, greetMsg
|
|
1786
|
+
tunnelconfig: pinggyOptionsToTunnelConfig(tunnelConfig, configid, tunnelName, tlsInfo, greetMsg),
|
|
1624
1787
|
status: this.buildStatus(tunnelid, status, "" /* NoError */),
|
|
1625
1788
|
stats
|
|
1626
1789
|
};
|
|
1627
1790
|
}
|
|
1791
|
+
async buildTunnelResponseV2(tunnelid, tunnelConfig, configFromCli, configid, tunnelName, serve) {
|
|
1792
|
+
const [status, stats, greetMsg, remoteurls] = await Promise.all([
|
|
1793
|
+
this.tunnelManager.getTunnelStatus(tunnelid),
|
|
1794
|
+
this.tunnelManager.getLatestTunnelStats(tunnelid) || newStats(),
|
|
1795
|
+
this.tunnelManager.getTunnelGreetMessage(tunnelid),
|
|
1796
|
+
this.tunnelManager.getTunnelUrls(tunnelid)
|
|
1797
|
+
]);
|
|
1798
|
+
return {
|
|
1799
|
+
tunnelid,
|
|
1800
|
+
remoteurls,
|
|
1801
|
+
tunnelconfig: pinggyOptionsToTunnelConfigV1(tunnelConfig, configFromCli),
|
|
1802
|
+
status: this.buildStatus(tunnelid, status, "" /* NoError */),
|
|
1803
|
+
stats,
|
|
1804
|
+
greetmsg: greetMsg
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1628
1807
|
error(code, err, fallback) {
|
|
1629
1808
|
return newErrorResponse({
|
|
1630
1809
|
code,
|
|
@@ -1635,19 +1814,28 @@ var TunnelOperations = class {
|
|
|
1635
1814
|
async handleStart(config) {
|
|
1636
1815
|
try {
|
|
1637
1816
|
const opts = tunnelConfigToPinggyOptions(config);
|
|
1638
|
-
const
|
|
1639
|
-
const { tunnelid, instance, tunnelName, additionalForwarding, serve } = await this.tunnelManager.createTunnel({
|
|
1817
|
+
const { tunnelid, instance, tunnelName, serve, tunnelConfig } = await this.tunnelManager.createTunnel({
|
|
1640
1818
|
...opts,
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
serve: config.serve
|
|
1819
|
+
configId: config.configid,
|
|
1820
|
+
name: config.configname,
|
|
1821
|
+
optional: {
|
|
1822
|
+
serve: config.serve
|
|
1823
|
+
}
|
|
1647
1824
|
});
|
|
1648
|
-
this.tunnelManager.startTunnel(tunnelid);
|
|
1825
|
+
await this.tunnelManager.startTunnel(tunnelid);
|
|
1649
1826
|
const tunnelPconfig = await this.tunnelManager.getTunnelConfig("", tunnelid);
|
|
1650
|
-
const resp = this.buildTunnelResponse(tunnelid, tunnelPconfig, config.configid, tunnelName,
|
|
1827
|
+
const resp = this.buildTunnelResponse(tunnelid, tunnelPconfig, config.configid, tunnelName, serve);
|
|
1828
|
+
return resp;
|
|
1829
|
+
} catch (err) {
|
|
1830
|
+
return this.error(ErrorCode.ErrorStartingTunnel, err, "Unknown error occurred while starting tunnel");
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
async handleStartV2(config) {
|
|
1834
|
+
try {
|
|
1835
|
+
const { tunnelid, instance, serve } = await this.tunnelManager.createTunnel(config);
|
|
1836
|
+
await this.tunnelManager.startTunnel(tunnelid);
|
|
1837
|
+
const tunnelPconfig = await this.tunnelManager.getTunnelConfig("", tunnelid);
|
|
1838
|
+
const resp = this.buildTunnelResponseV2(tunnelid, tunnelPconfig, config, config.configId, config.name, config.serve);
|
|
1651
1839
|
return resp;
|
|
1652
1840
|
} catch (err) {
|
|
1653
1841
|
return this.error(ErrorCode.ErrorStartingTunnel, err, "Unknown error occurred while starting tunnel");
|
|
@@ -1658,20 +1846,59 @@ var TunnelOperations = class {
|
|
|
1658
1846
|
const opts = tunnelConfigToPinggyOptions(config);
|
|
1659
1847
|
const tunnel = await this.tunnelManager.updateConfig({
|
|
1660
1848
|
...opts,
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
serve: config.serve
|
|
1849
|
+
configId: config.configid,
|
|
1850
|
+
name: config.configname,
|
|
1851
|
+
optional: {
|
|
1852
|
+
serve: config.serve
|
|
1853
|
+
}
|
|
1667
1854
|
});
|
|
1668
1855
|
if (!tunnel.instance || !tunnel.tunnelConfig)
|
|
1669
1856
|
throw new Error("Invalid tunnel state after configuration update");
|
|
1670
|
-
return this.buildTunnelResponse(tunnel.tunnelid, tunnel.tunnelConfig, config.configid, tunnel.tunnelName, tunnel.
|
|
1857
|
+
return this.buildTunnelResponse(tunnel.tunnelid, tunnel.tunnelConfig, config.configid, tunnel.tunnelName, tunnel.serve);
|
|
1671
1858
|
} catch (err) {
|
|
1672
1859
|
return this.error(ErrorCode.InternalServerError, err, "Failed to update tunnel configuration");
|
|
1673
1860
|
}
|
|
1674
1861
|
}
|
|
1862
|
+
async handleUpdateConfigV2(config) {
|
|
1863
|
+
try {
|
|
1864
|
+
const tunnel = await this.tunnelManager.updateConfig(config);
|
|
1865
|
+
if (!tunnel.instance || !tunnel.tunnelConfig)
|
|
1866
|
+
throw new Error("Invalid tunnel state after configuration update");
|
|
1867
|
+
return this.buildTunnelResponseV2(tunnel.tunnelid, tunnel.tunnelConfig, config, config.configId, tunnel.tunnelName, tunnel.serve);
|
|
1868
|
+
} catch (err) {
|
|
1869
|
+
return this.error(ErrorCode.InternalServerError, err, "Failed to update tunnel configuration");
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
async handleListV2() {
|
|
1873
|
+
try {
|
|
1874
|
+
const tunnels = await this.tunnelManager.getAllTunnels();
|
|
1875
|
+
if (tunnels.length === 0) {
|
|
1876
|
+
return [];
|
|
1877
|
+
}
|
|
1878
|
+
return Promise.all(
|
|
1879
|
+
tunnels.map(async (t) => {
|
|
1880
|
+
const rawStats = this.tunnelManager.getLatestTunnelStats(t.tunnelid) || newStats();
|
|
1881
|
+
const [status, tlsInfo, greetMsg] = await Promise.all([
|
|
1882
|
+
this.tunnelManager.getTunnelStatus(t.tunnelid),
|
|
1883
|
+
this.tunnelManager.getLocalserverTlsInfo(t.tunnelid),
|
|
1884
|
+
this.tunnelManager.getTunnelGreetMessage(t.tunnelid)
|
|
1885
|
+
]);
|
|
1886
|
+
const tunnelConfguration = status !== "closed" /* Closed */ && status !== "exited" /* Exited */ ? await this.tunnelManager.getTunnelConfig("", t.tunnelid) : t.tunnelConfig;
|
|
1887
|
+
const tunnelConfig = pinggyOptionsToTunnelConfigV1(tunnelConfguration, t.tunnelConfig);
|
|
1888
|
+
return {
|
|
1889
|
+
tunnelid: t.tunnelid,
|
|
1890
|
+
remoteurls: t.remoteurls,
|
|
1891
|
+
status: this.buildStatus(t.tunnelid, status, "" /* NoError */),
|
|
1892
|
+
stats: rawStats,
|
|
1893
|
+
tunnelconfig: tunnelConfig,
|
|
1894
|
+
greetmsg: greetMsg
|
|
1895
|
+
};
|
|
1896
|
+
})
|
|
1897
|
+
);
|
|
1898
|
+
} catch (err) {
|
|
1899
|
+
return this.error(ErrorCode.InternalServerError, err, "Failed to list tunnels");
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1675
1902
|
async handleList() {
|
|
1676
1903
|
try {
|
|
1677
1904
|
const tunnels = await this.tunnelManager.getAllTunnels();
|
|
@@ -1687,7 +1914,7 @@ var TunnelOperations = class {
|
|
|
1687
1914
|
this.tunnelManager.getTunnelGreetMessage(t.tunnelid)
|
|
1688
1915
|
]);
|
|
1689
1916
|
const pinggyOptions = status !== "closed" /* Closed */ && status !== "exited" /* Exited */ ? await this.tunnelManager.getTunnelConfig("", t.tunnelid) : t.tunnelConfig;
|
|
1690
|
-
const tunnelConfig = pinggyOptionsToTunnelConfig(pinggyOptions, t.
|
|
1917
|
+
const tunnelConfig = pinggyOptionsToTunnelConfig(pinggyOptions, t.configId, t.tunnelName, tlsInfo, greetMsg, t.serve);
|
|
1691
1918
|
return {
|
|
1692
1919
|
tunnelid: t.tunnelid,
|
|
1693
1920
|
remoteurls: t.remoteurls,
|
|
@@ -1703,10 +1930,10 @@ var TunnelOperations = class {
|
|
|
1703
1930
|
}
|
|
1704
1931
|
async handleStop(tunnelid) {
|
|
1705
1932
|
try {
|
|
1706
|
-
const {
|
|
1933
|
+
const { configId } = this.tunnelManager.stopTunnel(tunnelid);
|
|
1707
1934
|
const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
|
|
1708
1935
|
if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
|
|
1709
|
-
return this.buildTunnelResponse(tunnelid, managed.tunnelConfig,
|
|
1936
|
+
return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, configId, managed.tunnelName, managed.serve);
|
|
1710
1937
|
} catch (err) {
|
|
1711
1938
|
return this.error(ErrorCode.TunnelNotFound, err, "Failed to stop tunnel");
|
|
1712
1939
|
}
|
|
@@ -1715,7 +1942,7 @@ var TunnelOperations = class {
|
|
|
1715
1942
|
try {
|
|
1716
1943
|
const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
|
|
1717
1944
|
if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
|
|
1718
|
-
return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, managed.
|
|
1945
|
+
return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, managed.configId, managed.tunnelName, managed.serve);
|
|
1719
1946
|
} catch (err) {
|
|
1720
1947
|
return this.error(ErrorCode.TunnelNotFound, err, "Failed to get tunnel information");
|
|
1721
1948
|
}
|
|
@@ -1725,7 +1952,7 @@ var TunnelOperations = class {
|
|
|
1725
1952
|
await this.tunnelManager.restartTunnel(tunnelid);
|
|
1726
1953
|
const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
|
|
1727
1954
|
if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
|
|
1728
|
-
return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, managed.
|
|
1955
|
+
return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, managed.configId, managed.tunnelName, managed.serve);
|
|
1729
1956
|
} catch (err) {
|
|
1730
1957
|
return this.error(ErrorCode.TunnelNotFound, err, "Failed to restart tunnel");
|
|
1731
1958
|
}
|
|
@@ -1769,11 +1996,198 @@ var TunnelOperations = class {
|
|
|
1769
1996
|
// src/remote_management/remoteManagement.ts
|
|
1770
1997
|
import WebSocket from "ws";
|
|
1771
1998
|
|
|
1999
|
+
// src/remote_management/websocket_printer.ts
|
|
2000
|
+
import pico3 from "picocolors";
|
|
2001
|
+
var PENDING_START_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
2002
|
+
var RemoteManagementWebSocketPrinter = class {
|
|
2003
|
+
constructor() {
|
|
2004
|
+
this.tunnelManager = TunnelManager.getInstance();
|
|
2005
|
+
this.pendingStarts = /* @__PURE__ */ new Map();
|
|
2006
|
+
}
|
|
2007
|
+
setTunnelHandler(tunnelHandler) {
|
|
2008
|
+
this.tunnelHandler = tunnelHandler;
|
|
2009
|
+
}
|
|
2010
|
+
queueStart(config) {
|
|
2011
|
+
this.cleanupExpiredPendingStarts();
|
|
2012
|
+
const entry = {
|
|
2013
|
+
configId: this.getConfigIdFromRequest(config),
|
|
2014
|
+
configName: this.getConfigNameFromRequest(config),
|
|
2015
|
+
queuedAt: Date.now()
|
|
2016
|
+
};
|
|
2017
|
+
this.latestPendingConfigId = entry.configId;
|
|
2018
|
+
this.pendingStarts.set(entry.configId, entry);
|
|
2019
|
+
printer_default.startSpinner("Starting tunnel with config name: " + entry.configName);
|
|
2020
|
+
}
|
|
2021
|
+
failQueuedStart(config, reason) {
|
|
2022
|
+
const configId = this.getConfigIdFromRequest(config);
|
|
2023
|
+
const pending = this.pendingStarts.get(configId);
|
|
2024
|
+
const configName = pending?.configName || this.getConfigNameFromRequest(config);
|
|
2025
|
+
this.pendingStarts.delete(configId);
|
|
2026
|
+
if (this.latestPendingConfigId === configId) {
|
|
2027
|
+
this.latestPendingConfigId = void 0;
|
|
2028
|
+
printer_default.stopSpinnerFail(`Failed to start tunnel with config name: ${configName}. ${reason}`);
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
handleStartResult(config, result) {
|
|
2032
|
+
this.cleanupExpiredPendingStarts();
|
|
2033
|
+
const requestedConfigId = this.getConfigIdFromRequest(config);
|
|
2034
|
+
if (this.latestPendingConfigId && requestedConfigId !== this.latestPendingConfigId) {
|
|
2035
|
+
this.pendingStarts.delete(requestedConfigId);
|
|
2036
|
+
return;
|
|
2037
|
+
}
|
|
2038
|
+
if (isErrorResponse(result)) {
|
|
2039
|
+
this.failQueuedStart(config, result.message);
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
const configId = this.getConfigIdFromTunnel(result);
|
|
2043
|
+
const pending = this.pendingStarts.get(requestedConfigId) || {
|
|
2044
|
+
configId: requestedConfigId,
|
|
2045
|
+
configName: this.getConfigNameFromRequest(config),
|
|
2046
|
+
queuedAt: Date.now()
|
|
2047
|
+
};
|
|
2048
|
+
pending.tunnelId = result.tunnelid;
|
|
2049
|
+
this.pendingStarts.set(requestedConfigId, pending);
|
|
2050
|
+
if (result.remoteurls.length > 0) {
|
|
2051
|
+
this.completePendingStart(pending, result.remoteurls);
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
printStopRequested(tunnelId) {
|
|
2055
|
+
const details = this.resolveTunnelDetails(tunnelId);
|
|
2056
|
+
printer_default.startSpinner("Stopping tunnel with config name: " + details.configName);
|
|
2057
|
+
}
|
|
2058
|
+
handleStopResult(tunnelId, result) {
|
|
2059
|
+
const details = this.resolveTunnelDetails(tunnelId, result);
|
|
2060
|
+
if (isErrorResponse(result)) {
|
|
2061
|
+
printer_default.stopSpinnerFail("Failed to stop tunnel with config name: " + details.configName);
|
|
2062
|
+
return;
|
|
2063
|
+
}
|
|
2064
|
+
this.pendingStarts.delete(details.configId);
|
|
2065
|
+
printer_default.stopSpinnerSuccess("Stopped tunnel with config name: " + details.configName);
|
|
2066
|
+
}
|
|
2067
|
+
printRestartRequested(tunnelId) {
|
|
2068
|
+
const details = this.resolveTunnelDetails(tunnelId);
|
|
2069
|
+
printer_default.startSpinner("Restarting tunnel with config name: " + details.configName);
|
|
2070
|
+
}
|
|
2071
|
+
handleRestartResult(tunnelId, result) {
|
|
2072
|
+
const details = this.resolveTunnelDetails(tunnelId, result);
|
|
2073
|
+
if (isErrorResponse(result)) {
|
|
2074
|
+
printer_default.warn(`Failed to restart tunnel with config name: ${details.configName}. ${result.message}`);
|
|
2075
|
+
printer_default.stopSpinnerFail("Failed to restart tunnel with config name: " + details.configName);
|
|
2076
|
+
return;
|
|
2077
|
+
}
|
|
2078
|
+
printer_default.stopSpinnerSuccess("Restarted tunnel with config name: " + details.configName);
|
|
2079
|
+
if (result.remoteurls?.length > 0) {
|
|
2080
|
+
printer_default.info(pico3.cyanBright("Remote URLs:"));
|
|
2081
|
+
(result.remoteurls ?? []).forEach(
|
|
2082
|
+
(url) => printer_default.print(" " + pico3.magentaBright(url))
|
|
2083
|
+
);
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
monitorList(result) {
|
|
2087
|
+
this.cleanupExpiredPendingStarts();
|
|
2088
|
+
if (!Array.isArray(result) || this.pendingStarts.size === 0 || !this.latestPendingConfigId) {
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2091
|
+
for (const tunnel of result) {
|
|
2092
|
+
const pending = this.findPendingStart(tunnel);
|
|
2093
|
+
if (!pending) {
|
|
2094
|
+
continue;
|
|
2095
|
+
}
|
|
2096
|
+
if (pending.configId !== this.latestPendingConfigId) {
|
|
2097
|
+
continue;
|
|
2098
|
+
}
|
|
2099
|
+
pending.tunnelId = tunnel.tunnelid;
|
|
2100
|
+
this.pendingStarts.set(pending.configId, pending);
|
|
2101
|
+
if (tunnel.remoteurls.length > 0) {
|
|
2102
|
+
this.completePendingStart(pending, tunnel.remoteurls);
|
|
2103
|
+
continue;
|
|
2104
|
+
}
|
|
2105
|
+
if (tunnel.status.state === "exited" /* Exited */) {
|
|
2106
|
+
const reason = tunnel.status.errormsg || "Tunnel exited before a public URL was assigned";
|
|
2107
|
+
this.pendingStarts.delete(pending.configId);
|
|
2108
|
+
this.latestPendingConfigId = void 0;
|
|
2109
|
+
printer_default.stopSpinnerFail(`Tunnel start did not complete for config name: ${pending.configName}. ${reason}`);
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
completePendingStart(entry, urls) {
|
|
2114
|
+
if (this.latestPendingConfigId && entry.configId !== this.latestPendingConfigId) {
|
|
2115
|
+
this.pendingStarts.delete(entry.configId);
|
|
2116
|
+
return;
|
|
2117
|
+
}
|
|
2118
|
+
this.pendingStarts.delete(entry.configId);
|
|
2119
|
+
this.latestPendingConfigId = void 0;
|
|
2120
|
+
printer_default.stopSpinnerSuccess(`Tunnel started with config name: ${entry.configName}.`);
|
|
2121
|
+
printer_default.info(pico3.cyanBright("Remote URLs:"));
|
|
2122
|
+
(urls ?? []).forEach(
|
|
2123
|
+
(url) => printer_default.print(" " + pico3.magentaBright(url))
|
|
2124
|
+
);
|
|
2125
|
+
}
|
|
2126
|
+
cleanupExpiredPendingStarts() {
|
|
2127
|
+
const now = Date.now();
|
|
2128
|
+
for (const [configId, entry] of this.pendingStarts.entries()) {
|
|
2129
|
+
if (now - entry.queuedAt <= PENDING_START_TIMEOUT_MS) {
|
|
2130
|
+
continue;
|
|
2131
|
+
}
|
|
2132
|
+
this.pendingStarts.delete(configId);
|
|
2133
|
+
printer_default.warn(`Timed out while waiting for tunnel URL for config name: ${entry.configName}`);
|
|
2134
|
+
logger.warn("Pending websocket start entry expired", { configId, tunnelId: entry.tunnelId });
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
findPendingStart(tunnel) {
|
|
2138
|
+
const configId = this.getConfigIdFromTunnel(tunnel);
|
|
2139
|
+
const byConfigId = this.pendingStarts.get(configId);
|
|
2140
|
+
if (byConfigId) {
|
|
2141
|
+
return byConfigId;
|
|
2142
|
+
}
|
|
2143
|
+
for (const entry of this.pendingStarts.values()) {
|
|
2144
|
+
if (entry.tunnelId === tunnel.tunnelid) {
|
|
2145
|
+
return entry;
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
return void 0;
|
|
2149
|
+
}
|
|
2150
|
+
resolveTunnelDetails(tunnelId, result) {
|
|
2151
|
+
try {
|
|
2152
|
+
const managed = this.tunnelManager.getManagedTunnel(void 0, tunnelId);
|
|
2153
|
+
return {
|
|
2154
|
+
configId: managed.configId,
|
|
2155
|
+
configName: managed.tunnelName || managed.configId || tunnelId
|
|
2156
|
+
};
|
|
2157
|
+
} catch {
|
|
2158
|
+
if (result && !isErrorResponse(result)) {
|
|
2159
|
+
return {
|
|
2160
|
+
configId: this.getConfigIdFromTunnel(result),
|
|
2161
|
+
configName: this.getConfigNameFromTunnel(result)
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
return {
|
|
2165
|
+
configId: tunnelId,
|
|
2166
|
+
configName: tunnelId
|
|
2167
|
+
};
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
getConfigIdFromRequest(config) {
|
|
2171
|
+
return "configid" in config ? config.configid : config.configId;
|
|
2172
|
+
}
|
|
2173
|
+
getConfigNameFromRequest(config) {
|
|
2174
|
+
return "configname" in config ? config.configname : config.name;
|
|
2175
|
+
}
|
|
2176
|
+
getConfigIdFromTunnel(tunnel) {
|
|
2177
|
+
return "configid" in tunnel.tunnelconfig ? tunnel.tunnelconfig.configid : tunnel.tunnelconfig.configId;
|
|
2178
|
+
}
|
|
2179
|
+
getConfigNameFromTunnel(tunnel) {
|
|
2180
|
+
return "configname" in tunnel.tunnelconfig ? tunnel.tunnelconfig.configname : tunnel.tunnelconfig.name;
|
|
2181
|
+
}
|
|
2182
|
+
};
|
|
2183
|
+
var remoteManagementWebSocketPrinter = new RemoteManagementWebSocketPrinter();
|
|
2184
|
+
|
|
1772
2185
|
// src/remote_management/websocket_handlers.ts
|
|
1773
2186
|
import z2 from "zod";
|
|
1774
2187
|
var WebSocketCommandHandler = class {
|
|
1775
2188
|
constructor() {
|
|
1776
2189
|
this.tunnelHandler = new TunnelOperations();
|
|
2190
|
+
remoteManagementWebSocketPrinter.setTunnelHandler(this.tunnelHandler);
|
|
1777
2191
|
}
|
|
1778
2192
|
safeParse(text) {
|
|
1779
2193
|
if (!text) return void 0;
|
|
@@ -1798,35 +2212,157 @@ var WebSocketCommandHandler = class {
|
|
|
1798
2212
|
this.sendResponse(ws, resp);
|
|
1799
2213
|
}
|
|
1800
2214
|
async handleStartReq(req, raw) {
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
2215
|
+
let queuedConfig;
|
|
2216
|
+
try {
|
|
2217
|
+
const dc = StartSchema.parse(raw);
|
|
2218
|
+
queuedConfig = dc.tunnelConfig;
|
|
2219
|
+
remoteManagementWebSocketPrinter.queueStart(dc.tunnelConfig);
|
|
2220
|
+
const result = await this.tunnelHandler.handleStart(dc.tunnelConfig);
|
|
2221
|
+
remoteManagementWebSocketPrinter.handleStartResult(dc.tunnelConfig, result);
|
|
2222
|
+
return this.wrapResponse(result, req);
|
|
2223
|
+
} catch (e) {
|
|
2224
|
+
if (queuedConfig) {
|
|
2225
|
+
remoteManagementWebSocketPrinter.failQueuedStart(queuedConfig, String(e));
|
|
2226
|
+
}
|
|
2227
|
+
if (e instanceof z2.ZodError) {
|
|
2228
|
+
printer_default.warn("Validation failed for start request");
|
|
2229
|
+
return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
|
|
2230
|
+
}
|
|
2231
|
+
printer_default.warn(`Error in handleStartReq error: ${String(e)}`);
|
|
2232
|
+
return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
async handleStartV2Req(req, raw) {
|
|
2236
|
+
let queuedConfig;
|
|
2237
|
+
try {
|
|
2238
|
+
const dc = StartV2Schema.parse(raw);
|
|
2239
|
+
queuedConfig = dc.tunnelConfig;
|
|
2240
|
+
remoteManagementWebSocketPrinter.queueStart(dc.tunnelConfig);
|
|
2241
|
+
const result = await this.tunnelHandler.handleStartV2(dc.tunnelConfig);
|
|
2242
|
+
remoteManagementWebSocketPrinter.handleStartResult(dc.tunnelConfig, result);
|
|
2243
|
+
return this.wrapResponse(result, req);
|
|
2244
|
+
} catch (e) {
|
|
2245
|
+
if (queuedConfig) {
|
|
2246
|
+
remoteManagementWebSocketPrinter.failQueuedStart(queuedConfig, String(e));
|
|
2247
|
+
}
|
|
2248
|
+
if (e instanceof z2.ZodError) {
|
|
2249
|
+
printer_default.warn("Validation failed for start-v2 request");
|
|
2250
|
+
return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
|
|
2251
|
+
}
|
|
2252
|
+
printer_default.warn(`Error in handleStartV2Req error: ${String(e)}`);
|
|
2253
|
+
return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
|
|
2254
|
+
}
|
|
1805
2255
|
}
|
|
1806
2256
|
async handleStopReq(req, raw) {
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
2257
|
+
try {
|
|
2258
|
+
const dc = StopSchema.parse(raw);
|
|
2259
|
+
remoteManagementWebSocketPrinter.printStopRequested(dc.tunnelID);
|
|
2260
|
+
const result = await this.tunnelHandler.handleStop(dc.tunnelID);
|
|
2261
|
+
remoteManagementWebSocketPrinter.handleStopResult(dc.tunnelID, result);
|
|
2262
|
+
return this.wrapResponse(result, req);
|
|
2263
|
+
} catch (e) {
|
|
2264
|
+
if (e instanceof z2.ZodError) {
|
|
2265
|
+
printer_default.warn("Validation failed for stop request");
|
|
2266
|
+
return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
|
|
2267
|
+
}
|
|
2268
|
+
printer_default.warn(`Error in handleStopReq error: ${String(e)}`);
|
|
2269
|
+
return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
|
|
2270
|
+
}
|
|
1811
2271
|
}
|
|
1812
2272
|
async handleGetReq(req, raw) {
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
2273
|
+
try {
|
|
2274
|
+
const dc = GetSchema.parse(raw);
|
|
2275
|
+
const result = await this.tunnelHandler.handleGet(dc.tunnelID);
|
|
2276
|
+
return this.wrapResponse(result, req);
|
|
2277
|
+
} catch (e) {
|
|
2278
|
+
if (e instanceof z2.ZodError) {
|
|
2279
|
+
printer_default.warn("Validation failed for get request");
|
|
2280
|
+
return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
|
|
2281
|
+
}
|
|
2282
|
+
printer_default.warn(`Error in handleGetReq error: ${String(e)}`);
|
|
2283
|
+
return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
|
|
2284
|
+
}
|
|
1816
2285
|
}
|
|
1817
2286
|
async handleRestartReq(req, raw) {
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
2287
|
+
try {
|
|
2288
|
+
const dc = RestartSchema.parse(raw);
|
|
2289
|
+
remoteManagementWebSocketPrinter.printRestartRequested(dc.tunnelID);
|
|
2290
|
+
const result = await this.tunnelHandler.handleRestart(dc.tunnelID);
|
|
2291
|
+
remoteManagementWebSocketPrinter.handleRestartResult(dc.tunnelID, result);
|
|
2292
|
+
return this.wrapResponse(result, req);
|
|
2293
|
+
} catch (e) {
|
|
2294
|
+
if (e instanceof z2.ZodError) {
|
|
2295
|
+
printer_default.warn("Validation failed for restart request");
|
|
2296
|
+
return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
|
|
2297
|
+
}
|
|
2298
|
+
printer_default.warn(`Error in handleRestartReq error: ${String(e)}`);
|
|
2299
|
+
return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
|
|
2300
|
+
}
|
|
1821
2301
|
}
|
|
1822
2302
|
async handleUpdateConfigReq(req, raw) {
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
2303
|
+
try {
|
|
2304
|
+
const dc = UpdateConfigSchema.parse(raw);
|
|
2305
|
+
const result = await this.tunnelHandler.handleUpdateConfig(dc.tunnelConfig);
|
|
2306
|
+
return this.wrapResponse(result, req);
|
|
2307
|
+
} catch (e) {
|
|
2308
|
+
if (e instanceof z2.ZodError) {
|
|
2309
|
+
printer_default.warn("Validation failed for updateconfig request");
|
|
2310
|
+
return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
|
|
2311
|
+
}
|
|
2312
|
+
printer_default.warn(`Error in handleUpdateConfigReq error: ${String(e)}`);
|
|
2313
|
+
return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
async handleUpdateConfigV2Req(req, raw) {
|
|
2317
|
+
try {
|
|
2318
|
+
const dc = UpdateConfigV2Schema.parse(raw);
|
|
2319
|
+
const result = await this.tunnelHandler.handleUpdateConfigV2(dc.tunnelConfig);
|
|
2320
|
+
return this.wrapResponse(result, req);
|
|
2321
|
+
} catch (e) {
|
|
2322
|
+
if (e instanceof z2.ZodError) {
|
|
2323
|
+
printer_default.warn("Validation failed for update-config-v2 request");
|
|
2324
|
+
return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
|
|
2325
|
+
}
|
|
2326
|
+
printer_default.warn(`Error in handleUpdateConfigV2Req error: ${String(e)}`);
|
|
2327
|
+
return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
|
|
2328
|
+
}
|
|
1826
2329
|
}
|
|
1827
2330
|
async handleListReq(req) {
|
|
1828
|
-
|
|
1829
|
-
|
|
2331
|
+
try {
|
|
2332
|
+
const result = await this.tunnelHandler.handleList();
|
|
2333
|
+
remoteManagementWebSocketPrinter.monitorList(result);
|
|
2334
|
+
return this.wrapResponse(result, req);
|
|
2335
|
+
} catch (e) {
|
|
2336
|
+
printer_default.warn(`Error in handleListReq error: ${String(e)}`);
|
|
2337
|
+
return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
async handleListV2Req(req) {
|
|
2341
|
+
try {
|
|
2342
|
+
const result = await this.tunnelHandler.handleListV2();
|
|
2343
|
+
remoteManagementWebSocketPrinter.monitorList(result);
|
|
2344
|
+
return this.wrapResponse(result, req);
|
|
2345
|
+
} catch (e) {
|
|
2346
|
+
printer_default.warn(`Error in handleListV2Req error: ${String(e)}`);
|
|
2347
|
+
return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
async handleGetVersionReq(ws, req) {
|
|
2351
|
+
try {
|
|
2352
|
+
const versionResponse = {
|
|
2353
|
+
cli_version: getVersion()
|
|
2354
|
+
};
|
|
2355
|
+
const payload = {
|
|
2356
|
+
command: req.command,
|
|
2357
|
+
requestid: req.requestid,
|
|
2358
|
+
response: JSON.stringify(versionResponse),
|
|
2359
|
+
error: false
|
|
2360
|
+
};
|
|
2361
|
+
ws.send(JSON.stringify(payload));
|
|
2362
|
+
} catch (e) {
|
|
2363
|
+
printer_default.warn(`Error in handleGetVersionReq error: ${String(e)}`);
|
|
2364
|
+
this.sendError(ws, req, String(e));
|
|
2365
|
+
}
|
|
1830
2366
|
}
|
|
1831
2367
|
wrapResponse(result, req) {
|
|
1832
2368
|
if (isErrorResponse(result)) {
|
|
@@ -1860,6 +2396,10 @@ var WebSocketCommandHandler = class {
|
|
|
1860
2396
|
response = await this.handleStartReq(req, raw);
|
|
1861
2397
|
break;
|
|
1862
2398
|
}
|
|
2399
|
+
case "start-v2": {
|
|
2400
|
+
response = await this.handleStartV2Req(req, raw);
|
|
2401
|
+
break;
|
|
2402
|
+
}
|
|
1863
2403
|
case "stop": {
|
|
1864
2404
|
response = await this.handleStopReq(req, raw);
|
|
1865
2405
|
break;
|
|
@@ -1876,10 +2416,22 @@ var WebSocketCommandHandler = class {
|
|
|
1876
2416
|
response = await this.handleUpdateConfigReq(req, raw);
|
|
1877
2417
|
break;
|
|
1878
2418
|
}
|
|
2419
|
+
case "update-config-v2": {
|
|
2420
|
+
response = await this.handleUpdateConfigV2Req(req, raw);
|
|
2421
|
+
break;
|
|
2422
|
+
}
|
|
1879
2423
|
case "list": {
|
|
1880
2424
|
response = await this.handleListReq(req);
|
|
1881
2425
|
break;
|
|
1882
2426
|
}
|
|
2427
|
+
case "list-v2": {
|
|
2428
|
+
response = await this.handleListV2Req(req);
|
|
2429
|
+
break;
|
|
2430
|
+
}
|
|
2431
|
+
case "get-version": {
|
|
2432
|
+
await this.handleGetVersionReq(ws, req);
|
|
2433
|
+
return;
|
|
2434
|
+
}
|
|
1883
2435
|
default:
|
|
1884
2436
|
if (typeof req.command === "string") {
|
|
1885
2437
|
logger.warn("Unknown command", { command: req.command });
|
|
@@ -1898,6 +2450,18 @@ var WebSocketCommandHandler = class {
|
|
|
1898
2450
|
}
|
|
1899
2451
|
}
|
|
1900
2452
|
};
|
|
2453
|
+
function sendVersionResponse(ws) {
|
|
2454
|
+
const versionResponse = {
|
|
2455
|
+
cli_version: getVersion()
|
|
2456
|
+
};
|
|
2457
|
+
const payload = {
|
|
2458
|
+
command: "get-version",
|
|
2459
|
+
requestid: "0",
|
|
2460
|
+
response: JSON.stringify(versionResponse),
|
|
2461
|
+
error: false
|
|
2462
|
+
};
|
|
2463
|
+
ws.send(JSON.stringify(payload));
|
|
2464
|
+
}
|
|
1901
2465
|
function handleConnectionStatusMessage(firstMessage) {
|
|
1902
2466
|
try {
|
|
1903
2467
|
const text = typeof firstMessage === "string" ? firstMessage : firstMessage.toString();
|
|
@@ -1948,7 +2512,11 @@ async function parseRemoteManagement(values) {
|
|
|
1948
2512
|
if (typeof rmToken === "string" && rmToken.trim().length > 0) {
|
|
1949
2513
|
const manageHost = values["manage"];
|
|
1950
2514
|
try {
|
|
1951
|
-
|
|
2515
|
+
const remoteManagementConfig = {
|
|
2516
|
+
apiKey: rmToken,
|
|
2517
|
+
serverUrl: buildRemoteManagementWsUrl(manageHost)
|
|
2518
|
+
};
|
|
2519
|
+
await initiateRemoteManagement(remoteManagementConfig);
|
|
1952
2520
|
return { ok: true };
|
|
1953
2521
|
} catch (e) {
|
|
1954
2522
|
logger.error("Failed to initiate remote management:", e);
|
|
@@ -1956,11 +2524,11 @@ async function parseRemoteManagement(values) {
|
|
|
1956
2524
|
}
|
|
1957
2525
|
}
|
|
1958
2526
|
}
|
|
1959
|
-
async function initiateRemoteManagement(
|
|
1960
|
-
if (!
|
|
2527
|
+
async function initiateRemoteManagement(remoteManagementConfig) {
|
|
2528
|
+
if (!remoteManagementConfig.apiKey || remoteManagementConfig.apiKey.trim().length === 0) {
|
|
1961
2529
|
throw new Error("Remote management token is required (use --remote-management <TOKEN>)");
|
|
1962
2530
|
}
|
|
1963
|
-
const wsUrl =
|
|
2531
|
+
const wsUrl = remoteManagementConfig.serverUrl;
|
|
1964
2532
|
const wsHost = extractHostname(wsUrl);
|
|
1965
2533
|
logger.info("Remote management mode enabled.");
|
|
1966
2534
|
_stopRequested = false;
|
|
@@ -1976,7 +2544,7 @@ async function initiateRemoteManagement(token, manage) {
|
|
|
1976
2544
|
logConnecting();
|
|
1977
2545
|
setRemoteManagementState({ status: RemoteManagementStatus.Connecting, errorMessage: "" });
|
|
1978
2546
|
try {
|
|
1979
|
-
await handleWebSocketConnection(wsUrl, wsHost,
|
|
2547
|
+
await handleWebSocketConnection(wsUrl, wsHost, remoteManagementConfig.apiKey);
|
|
1980
2548
|
} catch (error) {
|
|
1981
2549
|
logger.warn("Remote management connection error", { error: String(error) });
|
|
1982
2550
|
}
|
|
@@ -2004,6 +2572,7 @@ async function handleWebSocketConnection(wsUrl, wsHost, token) {
|
|
|
2004
2572
|
};
|
|
2005
2573
|
ws.once("open", () => {
|
|
2006
2574
|
printer_default.success(`Connected to ${wsHost}`);
|
|
2575
|
+
setRemoteManagementState({ status: RemoteManagementStatus.Running, errorMessage: "" });
|
|
2007
2576
|
heartbeat = setInterval(() => {
|
|
2008
2577
|
if (ws.readyState === WebSocket.OPEN) ws.ping();
|
|
2009
2578
|
}, PING_INTERVAL_MS);
|
|
@@ -2014,7 +2583,11 @@ async function handleWebSocketConnection(wsUrl, wsHost, token) {
|
|
|
2014
2583
|
if (firstMessage) {
|
|
2015
2584
|
firstMessage = false;
|
|
2016
2585
|
const ok = handleConnectionStatusMessage(data);
|
|
2017
|
-
if (!ok)
|
|
2586
|
+
if (!ok) {
|
|
2587
|
+
ws.close();
|
|
2588
|
+
return;
|
|
2589
|
+
}
|
|
2590
|
+
sendVersionResponse(ws);
|
|
2018
2591
|
return;
|
|
2019
2592
|
}
|
|
2020
2593
|
setRemoteManagementState({ status: RemoteManagementStatus.Running, errorMessage: "" });
|
|
@@ -2025,20 +2598,21 @@ async function handleWebSocketConnection(wsUrl, wsHost, token) {
|
|
|
2025
2598
|
}
|
|
2026
2599
|
});
|
|
2027
2600
|
ws.on("unexpected-response", (_, res) => {
|
|
2028
|
-
setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: `HTTP ${res.statusCode}` });
|
|
2029
2601
|
if (res.statusCode === 401) {
|
|
2602
|
+
setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: `HTTP ${res.statusCode}` });
|
|
2030
2603
|
printer_default.error("Unauthorized. Please enter a valid token.");
|
|
2031
2604
|
logger.error("Unauthorized (401) on remote management connect");
|
|
2605
|
+
ws.close();
|
|
2032
2606
|
} else {
|
|
2607
|
+
logger.warn("Unexpected HTTP response ", { statusCode: res.statusCode });
|
|
2033
2608
|
printer_default.warn(`Unexpected HTTP ${res.statusCode}. Retrying...`);
|
|
2034
|
-
|
|
2609
|
+
cleanup();
|
|
2035
2610
|
}
|
|
2036
|
-
ws.close();
|
|
2037
2611
|
});
|
|
2038
2612
|
ws.on("close", (code, reason) => {
|
|
2039
2613
|
setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: "" });
|
|
2040
2614
|
logger.info("WebSocket closed", { code, reason: reason.toString() });
|
|
2041
|
-
printer_default.warn(`Disconnected (code: ${code}). Retrying...`);
|
|
2615
|
+
printer_default.warn(`Disconnected (code: ${code}). Retrying in ${RECONNECT_SLEEP_MS / 1e3}s...`);
|
|
2042
2616
|
cleanup();
|
|
2043
2617
|
});
|
|
2044
2618
|
ws.on("error", (err) => {
|