pinggy 0.3.4 → 0.3.6
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 +1 -1
- package/dist/chunk-65R2GMKQ.js +2101 -0
- package/dist/index.cjs +1814 -1362
- package/dist/index.d.cts +616 -0
- package/dist/index.d.ts +616 -0
- package/dist/index.js +38 -55
- package/dist/{main-CZY6GID4.js → main-2QDG7PWL.js} +229 -1726
- package/package.json +3 -4
- package/.github/workflows/npm-publish-github-packages.yml +0 -34
- package/.github/workflows/publish-binaries.yml +0 -223
- package/Makefile +0 -4
- package/caxa_build.js +0 -24
- package/dist/chunk-T5ESYDJY.js +0 -121
- package/ent.plist +0 -14
- package/jest.config.js +0 -19
- package/src/_tests_/build_config.test.ts +0 -91
- package/src/cli/buildConfig.ts +0 -475
- package/src/cli/defaults.ts +0 -20
- package/src/cli/extendedOptions.ts +0 -153
- package/src/cli/help.ts +0 -43
- package/src/cli/options.ts +0 -50
- package/src/cli/starCli.ts +0 -229
- package/src/index.ts +0 -30
- package/src/logger.ts +0 -138
- package/src/main.ts +0 -87
- package/src/remote_management/handler.ts +0 -244
- package/src/remote_management/remoteManagement.ts +0 -226
- package/src/remote_management/remote_schema.ts +0 -176
- package/src/remote_management/websocket_handlers.ts +0 -180
- package/src/tui/blessed/TunnelTui.ts +0 -340
- package/src/tui/blessed/components/DisplayUpdaters.ts +0 -189
- package/src/tui/blessed/components/KeyBindings.ts +0 -236
- package/src/tui/blessed/components/Modals.ts +0 -302
- package/src/tui/blessed/components/UIComponents.ts +0 -306
- package/src/tui/blessed/components/index.ts +0 -4
- package/src/tui/blessed/config.ts +0 -53
- package/src/tui/blessed/headerFetcher.ts +0 -42
- package/src/tui/blessed/index.ts +0 -2
- package/src/tui/blessed/qrCodeGenerator.ts +0 -20
- package/src/tui/blessed/webDebuggerConnection.ts +0 -128
- package/src/tui/ink/asciArt.ts +0 -7
- package/src/tui/ink/hooks/useQrCodes.ts +0 -27
- package/src/tui/ink/hooks/useReqResHeaders.ts +0 -27
- package/src/tui/ink/hooks/useTerminalSize.ts +0 -26
- package/src/tui/ink/hooks/useTerminalStats.ts +0 -24
- package/src/tui/ink/hooks/useWebDebugger.ts +0 -98
- package/src/tui/ink/index.tsx +0 -243
- package/src/tui/ink/layout/Borders.tsx +0 -15
- package/src/tui/ink/layout/Container.tsx +0 -15
- package/src/tui/ink/sections/DebuggerDetailModal.tsx +0 -53
- package/src/tui/ink/sections/KeyBindings.tsx +0 -58
- package/src/tui/ink/sections/QrCodeSection.tsx +0 -28
- package/src/tui/ink/sections/StatsSection.tsx +0 -20
- package/src/tui/ink/sections/URLsSection.tsx +0 -53
- package/src/tui/ink/utils/utils.ts +0 -35
- package/src/tui/spinner/spinner.ts +0 -64
- package/src/tunnel_manager/TunnelManager.ts +0 -1212
- package/src/types.ts +0 -255
- package/src/utils/FileServer.ts +0 -112
- package/src/utils/detect_vc_redist_on_windows.ts +0 -167
- package/src/utils/getFreePort.ts +0 -41
- package/src/utils/htmlTemplates.ts +0 -146
- package/src/utils/parseArgs.ts +0 -79
- package/src/utils/printer.ts +0 -81
- package/src/utils/util.ts +0 -18
- package/src/workers/file_serve_worker.ts +0 -33
- package/tsconfig.json +0 -17
- package/tsup.config.ts +0 -12
|
@@ -1,975 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
TunnelManager,
|
|
4
|
+
TunnelOperations,
|
|
5
|
+
closeRemoteManagement,
|
|
6
|
+
getRandomId,
|
|
7
|
+
getRemoteManagementState,
|
|
8
|
+
getVersion,
|
|
9
|
+
initiateRemoteManagement,
|
|
10
|
+
isValidPort,
|
|
11
|
+
parseRemoteManagement,
|
|
3
12
|
printer_default
|
|
4
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-65R2GMKQ.js";
|
|
5
14
|
import {
|
|
6
15
|
configureLogger,
|
|
7
16
|
enablePackageLogging,
|
|
8
17
|
logger
|
|
9
18
|
} from "./chunk-HUN2MRZO.js";
|
|
10
19
|
|
|
11
|
-
// src/tunnel_manager/TunnelManager.ts
|
|
12
|
-
import { pinggy } from "@pinggy/pinggy";
|
|
13
|
-
import path from "path";
|
|
14
|
-
import { Worker } from "worker_threads";
|
|
15
|
-
import { fileURLToPath } from "url";
|
|
16
|
-
|
|
17
|
-
// src/utils/util.ts
|
|
18
|
-
import { createRequire } from "module";
|
|
19
|
-
import { randomUUID } from "crypto";
|
|
20
|
-
function getRandomId() {
|
|
21
|
-
return randomUUID();
|
|
22
|
-
}
|
|
23
|
-
function isValidPort(p) {
|
|
24
|
-
return Number.isInteger(p) && p > 0 && p < 65536;
|
|
25
|
-
}
|
|
26
|
-
var require2 = createRequire(import.meta.url);
|
|
27
|
-
var pkg = require2("../package.json");
|
|
28
|
-
function getVersion() {
|
|
29
|
-
return pkg.version ?? "";
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// src/tunnel_manager/TunnelManager.ts
|
|
33
|
-
var __filename2 = fileURLToPath(import.meta.url);
|
|
34
|
-
var __dirname2 = path.dirname(__filename2);
|
|
35
|
-
var TunnelManager = class _TunnelManager {
|
|
36
|
-
constructor() {
|
|
37
|
-
this.tunnelsByTunnelId = /* @__PURE__ */ new Map();
|
|
38
|
-
this.tunnelsByConfigId = /* @__PURE__ */ new Map();
|
|
39
|
-
this.tunnelStats = /* @__PURE__ */ new Map();
|
|
40
|
-
this.tunnelStatsListeners = /* @__PURE__ */ new Map();
|
|
41
|
-
this.tunnelErrorListeners = /* @__PURE__ */ new Map();
|
|
42
|
-
this.tunnelDisconnectListeners = /* @__PURE__ */ new Map();
|
|
43
|
-
this.tunnelWorkerErrorListeners = /* @__PURE__ */ new Map();
|
|
44
|
-
this.tunnelStartListeners = /* @__PURE__ */ new Map();
|
|
45
|
-
}
|
|
46
|
-
static getInstance() {
|
|
47
|
-
if (!_TunnelManager.instance) {
|
|
48
|
-
_TunnelManager.instance = new _TunnelManager();
|
|
49
|
-
}
|
|
50
|
-
return _TunnelManager.instance;
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Creates a new managed tunnel instance with the given configuration.
|
|
54
|
-
* Builds the config with forwarding rules and creates the tunnel instance.
|
|
55
|
-
*
|
|
56
|
-
* @param config - The tunnel configuration options
|
|
57
|
-
* @param config.configid - Unique identifier for the tunnel configuration
|
|
58
|
-
* @param config.tunnelid - Optional custom tunnel identifier. If not provided, a random UUID will be generated
|
|
59
|
-
* @param config.additionalForwarding - Optional array of additional forwarding configurations
|
|
60
|
-
*
|
|
61
|
-
* @throws {Error} When configId is invalid or empty
|
|
62
|
-
* @throws {Error} When a tunnel with the given configId already exists
|
|
63
|
-
*
|
|
64
|
-
* @returns {ManagedTunnel} A new managed tunnel instance containing the tunnel details,
|
|
65
|
-
* status information, and statistics
|
|
66
|
-
*/
|
|
67
|
-
async createTunnel(config) {
|
|
68
|
-
const { configid, additionalForwarding, tunnelName } = config;
|
|
69
|
-
if (configid === void 0 || configid.trim().length === 0) {
|
|
70
|
-
throw new Error(`Invalid configId: "${configid}"`);
|
|
71
|
-
}
|
|
72
|
-
if (this.tunnelsByConfigId.has(configid)) {
|
|
73
|
-
throw new Error(`Tunnel with configId "${configid}" already exists`);
|
|
74
|
-
}
|
|
75
|
-
const tunnelid = config.tunnelid || getRandomId();
|
|
76
|
-
const configWithForwarding = this.buildPinggyConfig(config, additionalForwarding);
|
|
77
|
-
return this._createTunnelWithProcessedConfig({
|
|
78
|
-
configid,
|
|
79
|
-
tunnelid,
|
|
80
|
-
tunnelName,
|
|
81
|
-
originalConfig: config,
|
|
82
|
-
configWithForwarding,
|
|
83
|
-
additionalForwarding,
|
|
84
|
-
serve: config.serve,
|
|
85
|
-
autoReconnect: config.autoReconnect !== void 0 ? config.autoReconnect : false
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Internal method to create a tunnel with an already-processed configuration.
|
|
90
|
-
* This is used by createTunnel, restartTunnel, and updateConfig to avoid config processing.
|
|
91
|
-
*
|
|
92
|
-
* @param params - Configuration parameters with already-processed forwarding rules
|
|
93
|
-
* @returns The created ManagedTunnel instance
|
|
94
|
-
* @private
|
|
95
|
-
*/
|
|
96
|
-
async _createTunnelWithProcessedConfig(params) {
|
|
97
|
-
let instance;
|
|
98
|
-
try {
|
|
99
|
-
instance = await pinggy.createTunnel(params.configWithForwarding);
|
|
100
|
-
} catch (e) {
|
|
101
|
-
logger.error("Error creating tunnel instance:", e);
|
|
102
|
-
throw e;
|
|
103
|
-
}
|
|
104
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
105
|
-
const managed = {
|
|
106
|
-
tunnelid: params.tunnelid,
|
|
107
|
-
configid: params.configid,
|
|
108
|
-
tunnelName: params.tunnelName,
|
|
109
|
-
instance,
|
|
110
|
-
tunnelConfig: params.originalConfig,
|
|
111
|
-
configWithForwarding: params.configWithForwarding,
|
|
112
|
-
additionalForwarding: params.additionalForwarding,
|
|
113
|
-
serve: params.serve,
|
|
114
|
-
warnings: [],
|
|
115
|
-
isStopped: false,
|
|
116
|
-
createdAt: now,
|
|
117
|
-
startedAt: null,
|
|
118
|
-
stoppedAt: null,
|
|
119
|
-
autoReconnect: params.autoReconnect
|
|
120
|
-
};
|
|
121
|
-
instance.setTunnelEstablishedCallback(({}) => {
|
|
122
|
-
managed.startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
123
|
-
});
|
|
124
|
-
this.setupStatsCallback(params.tunnelid, managed);
|
|
125
|
-
this.setupErrorCallback(params.tunnelid, managed);
|
|
126
|
-
this.setupDisconnectCallback(params.tunnelid, managed);
|
|
127
|
-
this.setUpTunnelWorkerErrorCallback(params.tunnelid, managed);
|
|
128
|
-
this.tunnelsByTunnelId.set(params.tunnelid, managed);
|
|
129
|
-
this.tunnelsByConfigId.set(params.configid, managed);
|
|
130
|
-
logger.info("Tunnel created", { configid: params.configid, tunnelid: params.tunnelid });
|
|
131
|
-
return managed;
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* Builds the Pinggy configuration by merging the default forwarding rule
|
|
135
|
-
* with additional forwarding rules from additionalForwarding array.
|
|
136
|
-
*
|
|
137
|
-
* @param config - The base Pinggy configuration
|
|
138
|
-
* @param additionalForwarding - Optional array of additional forwarding rules
|
|
139
|
-
* @returns Modified PinggyOptions
|
|
140
|
-
*/
|
|
141
|
-
buildPinggyConfig(config, additionalForwarding) {
|
|
142
|
-
const forwardingRules = [];
|
|
143
|
-
if (config.forwarding) {
|
|
144
|
-
forwardingRules.push({
|
|
145
|
-
type: config.tunnelType && config.tunnelType[0] || "http",
|
|
146
|
-
address: config.forwarding
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
if (Array.isArray(additionalForwarding) && additionalForwarding.length > 0) {
|
|
150
|
-
for (const rule of additionalForwarding) {
|
|
151
|
-
if (rule && rule.localDomain && rule.localPort && rule.remoteDomain && isValidPort(rule.localPort)) {
|
|
152
|
-
const forwardingRule = {
|
|
153
|
-
type: rule.protocol,
|
|
154
|
-
// In Future we can make this dynamic based on user input
|
|
155
|
-
address: `${rule.localDomain}:${rule.localPort}`,
|
|
156
|
-
listenAddress: rule.remotePort && isValidPort(rule.remotePort) ? `${rule.remoteDomain}:${rule.remotePort}` : rule.remoteDomain
|
|
157
|
-
};
|
|
158
|
-
forwardingRules.push(forwardingRule);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
return {
|
|
163
|
-
...config,
|
|
164
|
-
forwarding: forwardingRules.length > 0 ? forwardingRules : config.forwarding
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* Start a tunnel that was created but not yet started
|
|
169
|
-
*/
|
|
170
|
-
async startTunnel(tunnelId) {
|
|
171
|
-
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
172
|
-
if (!managed) throw new Error(`Tunnel with id "${tunnelId}" not found`);
|
|
173
|
-
logger.info("Starting tunnel", { tunnelId });
|
|
174
|
-
let urls;
|
|
175
|
-
try {
|
|
176
|
-
urls = await managed.instance.start();
|
|
177
|
-
} catch (error) {
|
|
178
|
-
logger.error("Failed to start tunnel", { tunnelId, error });
|
|
179
|
-
throw error;
|
|
180
|
-
}
|
|
181
|
-
logger.info("Tunnel started", { tunnelId, urls });
|
|
182
|
-
if (managed.serve) {
|
|
183
|
-
this.startStaticFileServer(managed);
|
|
184
|
-
}
|
|
185
|
-
try {
|
|
186
|
-
const startListeners = this.tunnelStartListeners.get(tunnelId);
|
|
187
|
-
if (startListeners) {
|
|
188
|
-
for (const [id, listener] of startListeners) {
|
|
189
|
-
try {
|
|
190
|
-
listener(tunnelId, urls);
|
|
191
|
-
} catch (err) {
|
|
192
|
-
logger.debug("Error in start-listener callback", { listenerId: id, tunnelId, err });
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
} catch (e) {
|
|
197
|
-
logger.warn("Failed to notify start listeners", { tunnelId, e });
|
|
198
|
-
}
|
|
199
|
-
return urls;
|
|
200
|
-
}
|
|
201
|
-
/**
|
|
202
|
-
* Stops a running tunnel and updates its status.
|
|
203
|
-
*
|
|
204
|
-
* @param tunnelId - The unique identifier of the tunnel to stop
|
|
205
|
-
* @throws {Error} If the tunnel with the given tunnelId is not found
|
|
206
|
-
* @remarks
|
|
207
|
-
* - Clears the tunnel's remote URLs
|
|
208
|
-
* - Updates the tunnel's state to Exited if stopped successfully
|
|
209
|
-
* - Logs the stop operation with tunnelId and configId
|
|
210
|
-
*/
|
|
211
|
-
stopTunnel(tunnelId) {
|
|
212
|
-
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
213
|
-
if (!managed) throw new Error(`Tunnel "${tunnelId}" not found`);
|
|
214
|
-
logger.info("Stopping tunnel", { tunnelId, configId: managed.configid });
|
|
215
|
-
try {
|
|
216
|
-
managed.instance.stop();
|
|
217
|
-
if (managed.serveWorker) {
|
|
218
|
-
logger.info("terminating serveWorker");
|
|
219
|
-
managed.serveWorker.terminate();
|
|
220
|
-
}
|
|
221
|
-
this.tunnelStats.delete(tunnelId);
|
|
222
|
-
this.tunnelStatsListeners.delete(tunnelId);
|
|
223
|
-
this.tunnelStats.delete(tunnelId);
|
|
224
|
-
this.tunnelStatsListeners.delete(tunnelId);
|
|
225
|
-
this.tunnelErrorListeners.delete(tunnelId);
|
|
226
|
-
this.tunnelDisconnectListeners.delete(tunnelId);
|
|
227
|
-
this.tunnelWorkerErrorListeners.delete(tunnelId);
|
|
228
|
-
this.tunnelStartListeners.delete(tunnelId);
|
|
229
|
-
managed.serveWorker = null;
|
|
230
|
-
managed.warnings = managed.warnings ?? [];
|
|
231
|
-
managed.isStopped = true;
|
|
232
|
-
managed.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
233
|
-
logger.info("Tunnel stopped", { tunnelId, configId: managed.configid });
|
|
234
|
-
return { configid: managed.configid, tunnelid: managed.tunnelid };
|
|
235
|
-
} catch (error) {
|
|
236
|
-
logger.error("Failed to stop tunnel", { tunnelId, error });
|
|
237
|
-
throw error;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
/**
|
|
241
|
-
* Get all public URLs for a tunnel
|
|
242
|
-
*/
|
|
243
|
-
async getTunnelUrls(tunnelId) {
|
|
244
|
-
try {
|
|
245
|
-
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
246
|
-
if (!managed || managed.isStopped) {
|
|
247
|
-
logger.error(`Tunnel "${tunnelId}" not found when fetching URLs`);
|
|
248
|
-
return [];
|
|
249
|
-
}
|
|
250
|
-
const urls = await managed.instance.urls();
|
|
251
|
-
logger.debug("Queried tunnel URLs", { tunnelId, urls });
|
|
252
|
-
return urls;
|
|
253
|
-
} catch (error) {
|
|
254
|
-
logger.error("Error fetching tunnel URLs", { tunnelId, error });
|
|
255
|
-
throw error;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
/**
|
|
259
|
-
* Get all TunnelStatus currently managed by this TunnelManager
|
|
260
|
-
* @returns An array of all TunnelStatus objects
|
|
261
|
-
*/
|
|
262
|
-
async getAllTunnels() {
|
|
263
|
-
try {
|
|
264
|
-
const tunnelList = await Promise.all(Array.from(this.tunnelsByTunnelId.values()).map(async (tunnel) => {
|
|
265
|
-
return {
|
|
266
|
-
tunnelid: tunnel.tunnelid,
|
|
267
|
-
configid: tunnel.configid,
|
|
268
|
-
tunnelName: tunnel.tunnelName,
|
|
269
|
-
tunnelConfig: tunnel.tunnelConfig,
|
|
270
|
-
remoteurls: !tunnel.isStopped ? await this.getTunnelUrls(tunnel.tunnelid) : [],
|
|
271
|
-
additionalForwarding: tunnel.additionalForwarding,
|
|
272
|
-
serve: tunnel.serve
|
|
273
|
-
};
|
|
274
|
-
}));
|
|
275
|
-
return tunnelList;
|
|
276
|
-
} catch (err) {
|
|
277
|
-
logger.error("Error fetching tunnels", { error: err });
|
|
278
|
-
return [];
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
/**
|
|
282
|
-
* Get status of a tunnel
|
|
283
|
-
*/
|
|
284
|
-
async getTunnelStatus(tunnelId) {
|
|
285
|
-
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
286
|
-
if (!managed) {
|
|
287
|
-
logger.error(`Tunnel "${tunnelId}" not found when fetching status`);
|
|
288
|
-
throw new Error(`Tunnel "${tunnelId}" not found`);
|
|
289
|
-
}
|
|
290
|
-
if (managed.isStopped) {
|
|
291
|
-
return "exited";
|
|
292
|
-
}
|
|
293
|
-
const status = await managed.instance.getStatus();
|
|
294
|
-
logger.debug("Queried tunnel status", { tunnelId, status });
|
|
295
|
-
return status;
|
|
296
|
-
}
|
|
297
|
-
/**
|
|
298
|
-
* Stop all tunnels
|
|
299
|
-
*/
|
|
300
|
-
stopAllTunnels() {
|
|
301
|
-
for (const { instance } of this.tunnelsByTunnelId.values()) {
|
|
302
|
-
try {
|
|
303
|
-
instance.stop();
|
|
304
|
-
} catch (e) {
|
|
305
|
-
logger.warn("Error stopping tunnel instance", e);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
this.tunnelsByTunnelId.clear();
|
|
309
|
-
this.tunnelsByConfigId.clear();
|
|
310
|
-
this.tunnelStats.clear();
|
|
311
|
-
this.tunnelStatsListeners.clear();
|
|
312
|
-
logger.info("All tunnels stopped and cleared");
|
|
313
|
-
}
|
|
314
|
-
/**
|
|
315
|
-
* Remove a stopped tunnel's records so it will no longer be returned by list methods.
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
* @param tunnelId - the tunnel id to remove
|
|
319
|
-
* @returns true if the record was removed, false otherwise
|
|
320
|
-
*/
|
|
321
|
-
removeStoppedTunnelByTunnelId(tunnelId) {
|
|
322
|
-
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
323
|
-
if (!managed) {
|
|
324
|
-
logger.debug("Attempted to remove non-existent tunnel", { tunnelId });
|
|
325
|
-
return false;
|
|
326
|
-
}
|
|
327
|
-
if (!managed.isStopped) {
|
|
328
|
-
logger.warn("Attempted to remove tunnel that is not stopped", { tunnelId });
|
|
329
|
-
return false;
|
|
330
|
-
}
|
|
331
|
-
this._cleanupTunnelRecords(managed);
|
|
332
|
-
logger.info("Removed stopped tunnel records", { tunnelId, configId: managed.configid });
|
|
333
|
-
return true;
|
|
334
|
-
}
|
|
335
|
-
/**
|
|
336
|
-
* Remove a stopped tunnel by its config id.
|
|
337
|
-
* @param configId - the config id to remove
|
|
338
|
-
* @returns true if the record was removed, false otherwise
|
|
339
|
-
*/
|
|
340
|
-
removeStoppedTunnelByConfigId(configId) {
|
|
341
|
-
const managed = this.tunnelsByConfigId.get(configId);
|
|
342
|
-
if (!managed) {
|
|
343
|
-
logger.debug("Attempted to remove non-existent tunnel by configId", { configId });
|
|
344
|
-
return false;
|
|
345
|
-
}
|
|
346
|
-
return this.removeStoppedTunnelByTunnelId(managed.tunnelid);
|
|
347
|
-
}
|
|
348
|
-
_cleanupTunnelRecords(managed) {
|
|
349
|
-
if (!managed.isStopped) {
|
|
350
|
-
throw new Error(`Active tunnel "${managed.tunnelid}" cannot be removed`);
|
|
351
|
-
}
|
|
352
|
-
try {
|
|
353
|
-
if (managed.serveWorker) {
|
|
354
|
-
managed.serveWorker = null;
|
|
355
|
-
}
|
|
356
|
-
this.tunnelStats.delete(managed.tunnelid);
|
|
357
|
-
this.tunnelStatsListeners.delete(managed.tunnelid);
|
|
358
|
-
this.tunnelErrorListeners.delete(managed.tunnelid);
|
|
359
|
-
this.tunnelDisconnectListeners.delete(managed.tunnelid);
|
|
360
|
-
this.tunnelWorkerErrorListeners.delete(managed.tunnelid);
|
|
361
|
-
this.tunnelStartListeners.delete(managed.tunnelid);
|
|
362
|
-
this.tunnelsByTunnelId.delete(managed.tunnelid);
|
|
363
|
-
this.tunnelsByConfigId.delete(managed.configid);
|
|
364
|
-
} catch (e) {
|
|
365
|
-
logger.warn("Failed cleaning up tunnel records", { tunnelId: managed.tunnelid, error: e });
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
/**
|
|
369
|
-
* Get tunnel instance by either configId or tunnelId
|
|
370
|
-
* @param configId - The configuration ID of the tunnel
|
|
371
|
-
* @param tunnelId - The tunnel ID
|
|
372
|
-
* @returns The tunnel instance
|
|
373
|
-
* @throws Error if neither configId nor tunnelId is provided, or if tunnel is not found
|
|
374
|
-
*/
|
|
375
|
-
getTunnelInstance(configId, tunnelId) {
|
|
376
|
-
if (configId) {
|
|
377
|
-
const managed = this.tunnelsByConfigId.get(configId);
|
|
378
|
-
if (!managed) throw new Error(`Tunnel "${configId}" not found`);
|
|
379
|
-
return managed.instance;
|
|
380
|
-
}
|
|
381
|
-
if (tunnelId) {
|
|
382
|
-
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
383
|
-
if (!managed) throw new Error(`Tunnel "${tunnelId}" not found`);
|
|
384
|
-
return managed.instance;
|
|
385
|
-
}
|
|
386
|
-
throw new Error(`Either configId or tunnelId must be provided`);
|
|
387
|
-
}
|
|
388
|
-
/**
|
|
389
|
-
* Get tunnel config by either configId or tunnelId
|
|
390
|
-
* @param configId - The configuration ID of the tunnel
|
|
391
|
-
* @param tunnelId - The tunnel ID
|
|
392
|
-
* @returns The tunnel config
|
|
393
|
-
* @throws Error if neither configId nor tunnelId is provided, or if tunnel is not found
|
|
394
|
-
*/
|
|
395
|
-
async getTunnelConfig(configId, tunnelId) {
|
|
396
|
-
if (configId) {
|
|
397
|
-
const managed = this.tunnelsByConfigId.get(configId);
|
|
398
|
-
if (!managed) {
|
|
399
|
-
throw new Error(`Tunnel with configId "${configId}" not found`);
|
|
400
|
-
}
|
|
401
|
-
return managed.instance.getConfig();
|
|
402
|
-
}
|
|
403
|
-
if (tunnelId) {
|
|
404
|
-
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
405
|
-
if (!managed) {
|
|
406
|
-
throw new Error(`Tunnel with tunnelId "${tunnelId}" not found`);
|
|
407
|
-
}
|
|
408
|
-
return managed.instance.getConfig();
|
|
409
|
-
}
|
|
410
|
-
throw new Error(`Either configId or tunnelId must be provided`);
|
|
411
|
-
}
|
|
412
|
-
/**
|
|
413
|
-
* Restarts a tunnel with its current configuration.
|
|
414
|
-
* This function will stop the tunnel if it's running and start it again.
|
|
415
|
-
* All configurations including additional forwarding rules are preserved.
|
|
416
|
-
*/
|
|
417
|
-
async restartTunnel(tunnelid) {
|
|
418
|
-
const existingTunnel = this.tunnelsByTunnelId.get(tunnelid);
|
|
419
|
-
if (!existingTunnel) {
|
|
420
|
-
throw new Error(`Tunnel "${tunnelid}" not found`);
|
|
421
|
-
}
|
|
422
|
-
logger.info("Initiating tunnel restart", {
|
|
423
|
-
tunnelId: tunnelid,
|
|
424
|
-
configId: existingTunnel.configid
|
|
425
|
-
});
|
|
426
|
-
try {
|
|
427
|
-
const tunnelName = existingTunnel.tunnelName;
|
|
428
|
-
const currentConfigId = existingTunnel.configid;
|
|
429
|
-
const currentConfig = existingTunnel.tunnelConfig;
|
|
430
|
-
const configWithForwarding = existingTunnel.configWithForwarding;
|
|
431
|
-
const additionalForwarding = existingTunnel.additionalForwarding;
|
|
432
|
-
const currentServe = existingTunnel.serve;
|
|
433
|
-
const autoReconnect = existingTunnel.autoReconnect || false;
|
|
434
|
-
this.tunnelsByTunnelId.delete(tunnelid);
|
|
435
|
-
this.tunnelsByConfigId.delete(existingTunnel.configid);
|
|
436
|
-
this.tunnelStats.delete(tunnelid);
|
|
437
|
-
this.tunnelStatsListeners.delete(tunnelid);
|
|
438
|
-
this.tunnelErrorListeners.delete(tunnelid);
|
|
439
|
-
this.tunnelDisconnectListeners.delete(tunnelid);
|
|
440
|
-
this.tunnelWorkerErrorListeners.delete(tunnelid);
|
|
441
|
-
this.tunnelStartListeners.delete(tunnelid);
|
|
442
|
-
const newTunnel = await this._createTunnelWithProcessedConfig({
|
|
443
|
-
configid: currentConfigId,
|
|
444
|
-
tunnelid,
|
|
445
|
-
tunnelName,
|
|
446
|
-
originalConfig: currentConfig,
|
|
447
|
-
configWithForwarding,
|
|
448
|
-
additionalForwarding,
|
|
449
|
-
serve: currentServe,
|
|
450
|
-
autoReconnect
|
|
451
|
-
});
|
|
452
|
-
if (existingTunnel.createdAt) {
|
|
453
|
-
newTunnel.createdAt = existingTunnel.createdAt;
|
|
454
|
-
}
|
|
455
|
-
await this.startTunnel(newTunnel.tunnelid);
|
|
456
|
-
} catch (error) {
|
|
457
|
-
logger.error("Failed to restart tunnel", {
|
|
458
|
-
tunnelid,
|
|
459
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
460
|
-
});
|
|
461
|
-
throw new Error(`Failed to restart tunnel: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
/**
|
|
465
|
-
* Updates the configuration of an existing tunnel.
|
|
466
|
-
*
|
|
467
|
-
* This method handles the process of updating a tunnel's configuration while preserving
|
|
468
|
-
* its state. If the tunnel is running, it will be stopped, updated, and restarted.
|
|
469
|
-
* In case of failure, it attempts to restore the original configuration.
|
|
470
|
-
*
|
|
471
|
-
* @param newConfig - The new configuration to apply, including configid and optional additional forwarding
|
|
472
|
-
*
|
|
473
|
-
* @returns Promise resolving to the updated ManagedTunnel
|
|
474
|
-
* @throws Error if the tunnel is not found or if the update process fails
|
|
475
|
-
*/
|
|
476
|
-
async updateConfig(newConfig) {
|
|
477
|
-
const { configid, tunnelName: newTunnelName, additionalForwarding } = newConfig;
|
|
478
|
-
if (!configid || configid.trim().length === 0) {
|
|
479
|
-
throw new Error(`Invalid configid: "${configid}"`);
|
|
480
|
-
}
|
|
481
|
-
const existingTunnel = this.tunnelsByConfigId.get(configid);
|
|
482
|
-
if (!existingTunnel) {
|
|
483
|
-
throw new Error(`Tunnel with config id "${configid}" not found`);
|
|
484
|
-
}
|
|
485
|
-
const isStopped = existingTunnel.isStopped;
|
|
486
|
-
const currentTunnelConfig = existingTunnel.tunnelConfig;
|
|
487
|
-
const currentConfigWithForwarding = existingTunnel.configWithForwarding;
|
|
488
|
-
const currentTunnelId = existingTunnel.tunnelid;
|
|
489
|
-
const currentTunnelConfigId = existingTunnel.configid;
|
|
490
|
-
const currentAdditionalForwarding = existingTunnel.additionalForwarding;
|
|
491
|
-
const currentTunnelName = existingTunnel.tunnelName;
|
|
492
|
-
const currentServe = existingTunnel.serve;
|
|
493
|
-
const currentAutoReconnect = existingTunnel.autoReconnect || false;
|
|
494
|
-
try {
|
|
495
|
-
if (!isStopped) {
|
|
496
|
-
existingTunnel.instance.stop();
|
|
497
|
-
}
|
|
498
|
-
this.tunnelsByTunnelId.delete(currentTunnelId);
|
|
499
|
-
this.tunnelsByConfigId.delete(currentTunnelConfigId);
|
|
500
|
-
const mergedBaseConfig = {
|
|
501
|
-
...newConfig,
|
|
502
|
-
configid,
|
|
503
|
-
tunnelName: newTunnelName !== void 0 ? newTunnelName : currentTunnelName,
|
|
504
|
-
serve: newConfig.serve !== void 0 ? newConfig.serve : currentServe
|
|
505
|
-
};
|
|
506
|
-
const newConfigWithForwarding = this.buildPinggyConfig(
|
|
507
|
-
mergedBaseConfig,
|
|
508
|
-
additionalForwarding !== void 0 ? additionalForwarding : currentAdditionalForwarding
|
|
509
|
-
);
|
|
510
|
-
const newTunnel = await this._createTunnelWithProcessedConfig({
|
|
511
|
-
configid,
|
|
512
|
-
tunnelid: currentTunnelId,
|
|
513
|
-
tunnelName: newTunnelName !== void 0 ? newTunnelName : currentTunnelName,
|
|
514
|
-
originalConfig: mergedBaseConfig,
|
|
515
|
-
configWithForwarding: newConfigWithForwarding,
|
|
516
|
-
additionalForwarding: additionalForwarding !== void 0 ? additionalForwarding : currentAdditionalForwarding,
|
|
517
|
-
serve: newConfig.serve !== void 0 ? newConfig.serve : currentServe,
|
|
518
|
-
autoReconnect: currentAutoReconnect
|
|
519
|
-
});
|
|
520
|
-
if (!isStopped) {
|
|
521
|
-
await this.startTunnel(newTunnel.tunnelid);
|
|
522
|
-
}
|
|
523
|
-
logger.info("Tunnel configuration updated", {
|
|
524
|
-
tunnelId: newTunnel.tunnelid,
|
|
525
|
-
configId: newTunnel.configid,
|
|
526
|
-
isStopped
|
|
527
|
-
});
|
|
528
|
-
return newTunnel;
|
|
529
|
-
} catch (error) {
|
|
530
|
-
logger.error("Error updating tunnel configuration", {
|
|
531
|
-
configId: configid,
|
|
532
|
-
error: error instanceof Error ? error.message : String(error)
|
|
533
|
-
});
|
|
534
|
-
try {
|
|
535
|
-
const originalTunnel = await this._createTunnelWithProcessedConfig({
|
|
536
|
-
configid: currentTunnelConfigId,
|
|
537
|
-
tunnelid: currentTunnelId,
|
|
538
|
-
tunnelName: currentTunnelName,
|
|
539
|
-
originalConfig: currentTunnelConfig,
|
|
540
|
-
configWithForwarding: currentConfigWithForwarding,
|
|
541
|
-
additionalForwarding: currentAdditionalForwarding,
|
|
542
|
-
serve: currentServe,
|
|
543
|
-
autoReconnect: currentAutoReconnect
|
|
544
|
-
});
|
|
545
|
-
if (!isStopped) {
|
|
546
|
-
await this.startTunnel(originalTunnel.tunnelid);
|
|
547
|
-
}
|
|
548
|
-
logger.warn("Restored original tunnel configuration after update failure", {
|
|
549
|
-
currentTunnelId,
|
|
550
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
551
|
-
});
|
|
552
|
-
} catch (restoreError) {
|
|
553
|
-
logger.error("Failed to restore original tunnel configuration", {
|
|
554
|
-
currentTunnelId,
|
|
555
|
-
error: restoreError instanceof Error ? restoreError.message : "Unknown error"
|
|
556
|
-
});
|
|
557
|
-
}
|
|
558
|
-
throw error;
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
/**
|
|
562
|
-
* Retrieve the ManagedTunnel object by either configId or tunnelId.
|
|
563
|
-
* Throws an error if neither id is provided or the tunnel is not found.
|
|
564
|
-
*/
|
|
565
|
-
getManagedTunnel(configId, tunnelId) {
|
|
566
|
-
if (configId) {
|
|
567
|
-
const managed = this.tunnelsByConfigId.get(configId);
|
|
568
|
-
if (!managed) throw new Error(`Tunnel "${configId}" not found`);
|
|
569
|
-
return managed;
|
|
570
|
-
}
|
|
571
|
-
if (tunnelId) {
|
|
572
|
-
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
573
|
-
if (!managed) throw new Error(`Tunnel "${tunnelId}" not found`);
|
|
574
|
-
return managed;
|
|
575
|
-
}
|
|
576
|
-
throw new Error(`Either configId or tunnelId must be provided`);
|
|
577
|
-
}
|
|
578
|
-
async getTunnelGreetMessage(tunnelId) {
|
|
579
|
-
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
580
|
-
if (!managed) {
|
|
581
|
-
logger.error(`Tunnel "${tunnelId}" not found when fetching greet message`);
|
|
582
|
-
return null;
|
|
583
|
-
}
|
|
584
|
-
try {
|
|
585
|
-
if (managed.isStopped) {
|
|
586
|
-
return null;
|
|
587
|
-
}
|
|
588
|
-
const messages = await managed.instance.getGreetMessage();
|
|
589
|
-
if (Array.isArray(messages)) {
|
|
590
|
-
return messages.join(" ");
|
|
591
|
-
}
|
|
592
|
-
return messages ?? null;
|
|
593
|
-
} catch (e) {
|
|
594
|
-
logger.error(
|
|
595
|
-
`Error fetching greet message for tunnel "${tunnelId}": ${e instanceof Error ? e.message : String(e)}`
|
|
596
|
-
);
|
|
597
|
-
return null;
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
getTunnelStats(tunnelId) {
|
|
601
|
-
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
602
|
-
if (!managed) {
|
|
603
|
-
return null;
|
|
604
|
-
}
|
|
605
|
-
const stats = this.tunnelStats.get(tunnelId);
|
|
606
|
-
return stats || null;
|
|
607
|
-
}
|
|
608
|
-
getLatestTunnelStats(tunnelId) {
|
|
609
|
-
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
610
|
-
if (!managed) {
|
|
611
|
-
return null;
|
|
612
|
-
}
|
|
613
|
-
const stats = this.tunnelStats.get(tunnelId);
|
|
614
|
-
if (stats && stats.length > 0) {
|
|
615
|
-
return stats[stats.length - 1];
|
|
616
|
-
}
|
|
617
|
-
return null;
|
|
618
|
-
}
|
|
619
|
-
/**
|
|
620
|
-
* Registers a listener function to receive tunnel statistics updates.
|
|
621
|
-
* The listener will be called whenever any tunnel's stats are updated.
|
|
622
|
-
*
|
|
623
|
-
* @param tunnelId - The tunnel ID to listen to stats for
|
|
624
|
-
* @param listener - Function that receives tunnelId and stats when updates occur
|
|
625
|
-
* @returns A unique listener ID that can be used to deregister the listener and tunnelId
|
|
626
|
-
*
|
|
627
|
-
* @throws {Error} When the specified tunnelId does not exist
|
|
628
|
-
*/
|
|
629
|
-
async registerStatsListener(tunnelId, listener) {
|
|
630
|
-
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
631
|
-
if (!managed) {
|
|
632
|
-
throw new Error(`Tunnel "${tunnelId}" not found`);
|
|
633
|
-
}
|
|
634
|
-
if (!this.tunnelStatsListeners.has(tunnelId)) {
|
|
635
|
-
this.tunnelStatsListeners.set(tunnelId, /* @__PURE__ */ new Map());
|
|
636
|
-
}
|
|
637
|
-
const listenerId = getRandomId();
|
|
638
|
-
const tunnelListeners = this.tunnelStatsListeners.get(tunnelId);
|
|
639
|
-
tunnelListeners.set(listenerId, listener);
|
|
640
|
-
logger.info("Stats listener registered for tunnel", { tunnelId, listenerId });
|
|
641
|
-
return [listenerId, tunnelId];
|
|
642
|
-
}
|
|
643
|
-
async registerErrorListener(tunnelId, listener) {
|
|
644
|
-
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
645
|
-
if (!managed) {
|
|
646
|
-
throw new Error(`Tunnel "${tunnelId}" not found`);
|
|
647
|
-
}
|
|
648
|
-
if (!this.tunnelErrorListeners.has(tunnelId)) {
|
|
649
|
-
this.tunnelErrorListeners.set(tunnelId, /* @__PURE__ */ new Map());
|
|
650
|
-
}
|
|
651
|
-
const listenerId = getRandomId();
|
|
652
|
-
const tunnelErrorListeners = this.tunnelErrorListeners.get(tunnelId);
|
|
653
|
-
tunnelErrorListeners.set(listenerId, listener);
|
|
654
|
-
logger.info("Error listener registered for tunnel", { tunnelId, listenerId });
|
|
655
|
-
return listenerId;
|
|
656
|
-
}
|
|
657
|
-
async registerDisconnectListener(tunnelId, listener) {
|
|
658
|
-
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
659
|
-
if (!managed) {
|
|
660
|
-
throw new Error(`Tunnel "${tunnelId}" not found`);
|
|
661
|
-
}
|
|
662
|
-
if (!this.tunnelDisconnectListeners.has(tunnelId)) {
|
|
663
|
-
this.tunnelDisconnectListeners.set(tunnelId, /* @__PURE__ */ new Map());
|
|
664
|
-
}
|
|
665
|
-
const listenerId = getRandomId();
|
|
666
|
-
const tunnelDisconnectListeners = this.tunnelDisconnectListeners.get(tunnelId);
|
|
667
|
-
tunnelDisconnectListeners.set(listenerId, listener);
|
|
668
|
-
logger.info("Disconnect listener registered for tunnel", { tunnelId, listenerId });
|
|
669
|
-
return listenerId;
|
|
670
|
-
}
|
|
671
|
-
async registerWorkerErrorListner(tunnelId, listener) {
|
|
672
|
-
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
673
|
-
if (!managed) {
|
|
674
|
-
throw new Error(`Tunnel "${tunnelId}" not found`);
|
|
675
|
-
}
|
|
676
|
-
if (!this.tunnelWorkerErrorListeners.has(tunnelId)) {
|
|
677
|
-
this.tunnelWorkerErrorListeners.set(tunnelId, /* @__PURE__ */ new Map());
|
|
678
|
-
}
|
|
679
|
-
const listenerId = getRandomId();
|
|
680
|
-
const tunnelWorkerErrorListner = this.tunnelWorkerErrorListeners.get(tunnelId);
|
|
681
|
-
tunnelWorkerErrorListner?.set(listenerId, listener);
|
|
682
|
-
logger.info("TunnelWorker error listener registered for tunnel", { tunnelId, listenerId });
|
|
683
|
-
}
|
|
684
|
-
async registerStartListener(tunnelId, listener) {
|
|
685
|
-
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
686
|
-
if (!managed) {
|
|
687
|
-
throw new Error(`Tunnel "${tunnelId}" not found`);
|
|
688
|
-
}
|
|
689
|
-
if (!this.tunnelStartListeners.has(tunnelId)) {
|
|
690
|
-
this.tunnelStartListeners.set(tunnelId, /* @__PURE__ */ new Map());
|
|
691
|
-
}
|
|
692
|
-
const listenerId = getRandomId();
|
|
693
|
-
const listeners = this.tunnelStartListeners.get(tunnelId);
|
|
694
|
-
listeners.set(listenerId, listener);
|
|
695
|
-
logger.info("Start listener registered for tunnel", { tunnelId, listenerId });
|
|
696
|
-
return listenerId;
|
|
697
|
-
}
|
|
698
|
-
/**
|
|
699
|
-
* Removes a previously registered stats listener.
|
|
700
|
-
*
|
|
701
|
-
* @param tunnelId - The tunnel ID the listener was registered for
|
|
702
|
-
* @param listenerId - The unique ID returned when the listener was registered
|
|
703
|
-
*/
|
|
704
|
-
deregisterStatsListener(tunnelId, listenerId) {
|
|
705
|
-
const tunnelListeners = this.tunnelStatsListeners.get(tunnelId);
|
|
706
|
-
if (!tunnelListeners) {
|
|
707
|
-
logger.warn("No listeners found for tunnel", { tunnelId });
|
|
708
|
-
return;
|
|
709
|
-
}
|
|
710
|
-
const removed = tunnelListeners.delete(listenerId);
|
|
711
|
-
if (removed) {
|
|
712
|
-
logger.info("Stats listener deregistered", { tunnelId, listenerId });
|
|
713
|
-
if (tunnelListeners.size === 0) {
|
|
714
|
-
this.tunnelStatsListeners.delete(tunnelId);
|
|
715
|
-
}
|
|
716
|
-
} else {
|
|
717
|
-
logger.warn("Attempted to deregister non-existent stats listener", { tunnelId, listenerId });
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
deregisterErrorListener(tunnelId, listenerId) {
|
|
721
|
-
const listeners = this.tunnelErrorListeners.get(tunnelId);
|
|
722
|
-
if (!listeners) {
|
|
723
|
-
logger.warn("No error listeners found for tunnel", { tunnelId });
|
|
724
|
-
return;
|
|
725
|
-
}
|
|
726
|
-
const removed = listeners.delete(listenerId);
|
|
727
|
-
if (removed) {
|
|
728
|
-
logger.info("Error listener deregistered", { tunnelId, listenerId });
|
|
729
|
-
if (listeners.size === 0) {
|
|
730
|
-
this.tunnelErrorListeners.delete(tunnelId);
|
|
731
|
-
}
|
|
732
|
-
} else {
|
|
733
|
-
logger.warn("Attempted to deregister non-existent error listener", { tunnelId, listenerId });
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
deregisterDisconnectListener(tunnelId, listenerId) {
|
|
737
|
-
const listeners = this.tunnelDisconnectListeners.get(tunnelId);
|
|
738
|
-
if (!listeners) {
|
|
739
|
-
logger.warn("No disconnect listeners found for tunnel", { tunnelId });
|
|
740
|
-
return;
|
|
741
|
-
}
|
|
742
|
-
const removed = listeners.delete(listenerId);
|
|
743
|
-
if (removed) {
|
|
744
|
-
logger.info("Disconnect listener deregistered", { tunnelId, listenerId });
|
|
745
|
-
if (listeners.size === 0) {
|
|
746
|
-
this.tunnelDisconnectListeners.delete(tunnelId);
|
|
747
|
-
}
|
|
748
|
-
} else {
|
|
749
|
-
logger.warn("Attempted to deregister non-existent disconnect listener", { tunnelId, listenerId });
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
async getLocalserverTlsInfo(tunnelId) {
|
|
753
|
-
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
754
|
-
if (!managed) {
|
|
755
|
-
logger.error(`Tunnel "${tunnelId}" not found when fetching local server TLS info`);
|
|
756
|
-
return false;
|
|
757
|
-
}
|
|
758
|
-
try {
|
|
759
|
-
if (managed.isStopped) {
|
|
760
|
-
return false;
|
|
761
|
-
}
|
|
762
|
-
const tlsInfo = await managed.instance.getLocalServerTls();
|
|
763
|
-
if (tlsInfo) {
|
|
764
|
-
return tlsInfo;
|
|
765
|
-
}
|
|
766
|
-
return false;
|
|
767
|
-
} catch (e) {
|
|
768
|
-
logger.error(`Error fetching TLS info for tunnel "${tunnelId}": ${e instanceof Error ? e.message : e}`);
|
|
769
|
-
return false;
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
/**
|
|
773
|
-
* Sets up the stats callback for a tunnel during creation.
|
|
774
|
-
* This callback will update stored stats and notify all registered listeners.
|
|
775
|
-
*/
|
|
776
|
-
setupStatsCallback(tunnelId, managed) {
|
|
777
|
-
try {
|
|
778
|
-
const callback = (usage) => {
|
|
779
|
-
this.updateStats(tunnelId, usage);
|
|
780
|
-
};
|
|
781
|
-
managed.instance.setUsageUpdateCallback(callback);
|
|
782
|
-
logger.debug("Stats callback set up for tunnel", { tunnelId });
|
|
783
|
-
} catch (error) {
|
|
784
|
-
logger.warn("Failed to set up stats callback", { tunnelId, error });
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
notifyErrorListeners(tunnelId, errorMsg, isFatal) {
|
|
788
|
-
try {
|
|
789
|
-
const listeners = this.tunnelErrorListeners.get(tunnelId);
|
|
790
|
-
if (!listeners) return;
|
|
791
|
-
for (const [id, listener] of listeners) {
|
|
792
|
-
try {
|
|
793
|
-
listener(tunnelId, errorMsg, isFatal);
|
|
794
|
-
} catch (err) {
|
|
795
|
-
logger.debug("Error in error-listener callback", { listenerId: id, tunnelId, err });
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
} catch (err) {
|
|
799
|
-
logger.debug("Failed to notify error listeners", { tunnelId, err });
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
setupErrorCallback(tunnelId, managed) {
|
|
803
|
-
try {
|
|
804
|
-
const callback = ({ errorNo, error, recoverable }) => {
|
|
805
|
-
try {
|
|
806
|
-
const msg = typeof error === "string" ? error : String(error);
|
|
807
|
-
const isFatal = true;
|
|
808
|
-
logger.debug("Tunnel reported error", { tunnelId, errorNo, errorMsg: msg, recoverable });
|
|
809
|
-
this.notifyErrorListeners(tunnelId, msg, isFatal);
|
|
810
|
-
} catch (e) {
|
|
811
|
-
logger.warn("Error handling tunnel error callback", { tunnelId, e });
|
|
812
|
-
}
|
|
813
|
-
};
|
|
814
|
-
managed.instance.setTunnelErrorCallback(callback);
|
|
815
|
-
logger.debug("Error callback set up for tunnel", { tunnelId });
|
|
816
|
-
} catch (error) {
|
|
817
|
-
logger.warn("Failed to set up error callback", { tunnelId, error });
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
setupDisconnectCallback(tunnelId, managed) {
|
|
821
|
-
try {
|
|
822
|
-
const callback = ({ error, messages }) => {
|
|
823
|
-
try {
|
|
824
|
-
logger.debug("Tunnel disconnected", { tunnelId, error, messages });
|
|
825
|
-
const managedTunnel = this.tunnelsByTunnelId.get(tunnelId);
|
|
826
|
-
if (managedTunnel) {
|
|
827
|
-
managedTunnel.isStopped = true;
|
|
828
|
-
managedTunnel.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
829
|
-
}
|
|
830
|
-
if (managedTunnel && managedTunnel.autoReconnect) {
|
|
831
|
-
logger.info("Auto-reconnecting tunnel", { tunnelId });
|
|
832
|
-
setTimeout(async () => {
|
|
833
|
-
try {
|
|
834
|
-
await this.restartTunnel(tunnelId);
|
|
835
|
-
logger.info("Tunnel auto-reconnected successfully", { tunnelId });
|
|
836
|
-
} catch (e) {
|
|
837
|
-
logger.error("Failed to auto-reconnect tunnel", { tunnelId, e });
|
|
838
|
-
}
|
|
839
|
-
}, 1e4);
|
|
840
|
-
}
|
|
841
|
-
const listeners = this.tunnelDisconnectListeners.get(tunnelId);
|
|
842
|
-
if (!listeners) return;
|
|
843
|
-
for (const [id, listener] of listeners) {
|
|
844
|
-
try {
|
|
845
|
-
listener(tunnelId, error, messages);
|
|
846
|
-
} catch (err) {
|
|
847
|
-
logger.debug("Error in disconnect-listener callback", { listenerId: id, tunnelId, err });
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
} catch (e) {
|
|
851
|
-
logger.warn("Error handling tunnel disconnect callback", { tunnelId, e });
|
|
852
|
-
}
|
|
853
|
-
};
|
|
854
|
-
managed.instance.setTunnelDisconnectedCallback(callback);
|
|
855
|
-
logger.debug("Disconnect callback set up for tunnel", { tunnelId });
|
|
856
|
-
} catch (error) {
|
|
857
|
-
logger.warn("Failed to set up disconnect callback", { tunnelId, error });
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
setUpTunnelWorkerErrorCallback(tunnelId, managed) {
|
|
861
|
-
try {
|
|
862
|
-
const callback = (error) => {
|
|
863
|
-
try {
|
|
864
|
-
logger.debug("Error in Tunnel Worker", { tunnelId, errorMessage: error.message });
|
|
865
|
-
const listeners = this.tunnelWorkerErrorListeners.get(tunnelId);
|
|
866
|
-
if (!listeners) return;
|
|
867
|
-
for (const [id, listener] of listeners) {
|
|
868
|
-
try {
|
|
869
|
-
listener(tunnelId, error);
|
|
870
|
-
} catch (err) {
|
|
871
|
-
logger.debug("Error in worker-error-listener callback", { listenerId: id, tunnelId, err });
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
} catch (e) {
|
|
875
|
-
logger.warn("Error handling tunnel worker error callback", { tunnelId, e });
|
|
876
|
-
}
|
|
877
|
-
};
|
|
878
|
-
managed.instance.setWorkerErrorCallback(callback);
|
|
879
|
-
logger.debug("Disconnect callback set up for tunnel", { tunnelId });
|
|
880
|
-
} catch (error) {
|
|
881
|
-
logger.warn("Failed to setup tunnel worker error callback");
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
/**
|
|
885
|
-
* Updates the stored stats for a tunnel and notifies all registered listeners.
|
|
886
|
-
*/
|
|
887
|
-
updateStats(tunnelId, rawUsage) {
|
|
888
|
-
try {
|
|
889
|
-
const normalizedStats = this.normalizeStats(rawUsage);
|
|
890
|
-
const existingStats = this.tunnelStats.get(tunnelId) || [];
|
|
891
|
-
const updatedStats = [...existingStats, normalizedStats];
|
|
892
|
-
this.tunnelStats.set(tunnelId, updatedStats);
|
|
893
|
-
const tunnelListeners = this.tunnelStatsListeners.get(tunnelId);
|
|
894
|
-
if (tunnelListeners) {
|
|
895
|
-
for (const [listenerId, listener] of tunnelListeners) {
|
|
896
|
-
try {
|
|
897
|
-
listener(tunnelId, normalizedStats);
|
|
898
|
-
} catch (error) {
|
|
899
|
-
logger.warn("Error in stats listener callback", { listenerId, tunnelId, error });
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
logger.debug("Stats updated and listeners notified", {
|
|
904
|
-
tunnelId,
|
|
905
|
-
listenersCount: tunnelListeners?.size || 0
|
|
906
|
-
});
|
|
907
|
-
} catch (error) {
|
|
908
|
-
logger.warn("Error updating stats", { tunnelId, error });
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
/**
|
|
912
|
-
* Normalizes raw usage data from the SDK into a consistent TunnelStats format.
|
|
913
|
-
*/
|
|
914
|
-
normalizeStats(rawStats) {
|
|
915
|
-
const elapsed = this.parseNumber(rawStats.elapsedTime ?? 0);
|
|
916
|
-
const liveConns = this.parseNumber(rawStats.numLiveConnections ?? 0);
|
|
917
|
-
const totalConns = this.parseNumber(rawStats.numTotalConnections ?? 0);
|
|
918
|
-
const reqBytes = this.parseNumber(rawStats.numTotalReqBytes ?? 0);
|
|
919
|
-
const resBytes = this.parseNumber(rawStats.numTotalResBytes ?? 0);
|
|
920
|
-
const txBytes = this.parseNumber(rawStats.numTotalTxBytes ?? 0);
|
|
921
|
-
return {
|
|
922
|
-
elapsedTime: elapsed,
|
|
923
|
-
numLiveConnections: liveConns,
|
|
924
|
-
numTotalConnections: totalConns,
|
|
925
|
-
numTotalReqBytes: reqBytes,
|
|
926
|
-
numTotalResBytes: resBytes,
|
|
927
|
-
numTotalTxBytes: txBytes
|
|
928
|
-
};
|
|
929
|
-
}
|
|
930
|
-
parseNumber(value) {
|
|
931
|
-
const parsed = typeof value === "number" ? value : parseInt(String(value), 10);
|
|
932
|
-
return isNaN(parsed) ? 0 : parsed;
|
|
933
|
-
}
|
|
934
|
-
startStaticFileServer(managed) {
|
|
935
|
-
try {
|
|
936
|
-
const __filename3 = fileURLToPath(import.meta.url);
|
|
937
|
-
const __dirname3 = path.dirname(__filename3);
|
|
938
|
-
const fileServerWorkerPath = path.join(__dirname3, "workers", "file_serve_worker.cjs");
|
|
939
|
-
const staticServerWorker = new Worker(fileServerWorkerPath, {
|
|
940
|
-
workerData: {
|
|
941
|
-
dir: managed.serve,
|
|
942
|
-
port: managed.tunnelConfig?.forwarding
|
|
943
|
-
}
|
|
944
|
-
});
|
|
945
|
-
staticServerWorker.on("message", (msg) => {
|
|
946
|
-
switch (msg.type) {
|
|
947
|
-
case "started":
|
|
948
|
-
logger.info("Static file server started", { dir: managed.serve });
|
|
949
|
-
break;
|
|
950
|
-
case "warning":
|
|
951
|
-
if (msg.code === "INVALID_TUNNEL_SERVE_PATH") {
|
|
952
|
-
managed.warnings = managed.warnings ?? [];
|
|
953
|
-
managed.warnings.push({ code: msg.code, message: msg.message });
|
|
954
|
-
}
|
|
955
|
-
printer_default.warn(msg.message);
|
|
956
|
-
break;
|
|
957
|
-
case "error":
|
|
958
|
-
managed.warnings = managed.warnings ?? [];
|
|
959
|
-
managed.warnings.push({
|
|
960
|
-
code: "UNKNOWN_WARNING",
|
|
961
|
-
message: msg.message
|
|
962
|
-
});
|
|
963
|
-
break;
|
|
964
|
-
}
|
|
965
|
-
});
|
|
966
|
-
managed.serveWorker = staticServerWorker;
|
|
967
|
-
} catch (error) {
|
|
968
|
-
logger.error("Error starting static file server", error);
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
};
|
|
972
|
-
|
|
973
20
|
// src/cli/options.ts
|
|
974
21
|
var cliOptions = {
|
|
975
22
|
// SSH-like options
|
|
@@ -995,7 +42,7 @@ var cliOptions = {
|
|
|
995
42
|
v: { type: "boolean", description: "Print logs to stdout for Cli. Overrides PINGGY_LOG_STDOUT environment variable" },
|
|
996
43
|
vv: { type: "boolean", description: "Enable detailed logging for the Node.js SDK and Libpinggy, including both info and debug level logs." },
|
|
997
44
|
vvv: { type: "boolean", description: "Enable all logs from Cli, SDK and internal components." },
|
|
998
|
-
autoreconnect: { type: "
|
|
45
|
+
autoreconnect: { type: "string", short: "a", description: "Automatically reconnect tunnel on failure (enabled by default). Use -a false to disable." },
|
|
999
46
|
// Save and load config
|
|
1000
47
|
saveconf: { type: "string", description: "Create the configuration file based on the options provided here" },
|
|
1001
48
|
conf: { type: "string", description: "Use the configuration file as base. Other options will be used to override this file" },
|
|
@@ -1065,7 +112,7 @@ var defaultOptions = {
|
|
|
1065
112
|
originalRequestUrl: false,
|
|
1066
113
|
allowPreflight: false,
|
|
1067
114
|
reverseProxy: false,
|
|
1068
|
-
autoReconnect:
|
|
115
|
+
autoReconnect: true
|
|
1069
116
|
};
|
|
1070
117
|
|
|
1071
118
|
// src/cli/extendedOptions.ts
|
|
@@ -1195,37 +242,71 @@ function isValidIpV6Cidr(input) {
|
|
|
1195
242
|
}
|
|
1196
243
|
|
|
1197
244
|
// src/cli/buildConfig.ts
|
|
1198
|
-
import { TunnelType
|
|
245
|
+
import { TunnelType } from "@pinggy/pinggy";
|
|
1199
246
|
import fs from "fs";
|
|
1200
|
-
import
|
|
247
|
+
import path from "path";
|
|
1201
248
|
var domainRegex = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
|
|
249
|
+
var KEYWORDS = /* @__PURE__ */ new Set([
|
|
250
|
+
TunnelType.Http,
|
|
251
|
+
TunnelType.Tcp,
|
|
252
|
+
TunnelType.Tls,
|
|
253
|
+
TunnelType.Udp,
|
|
254
|
+
TunnelType.TlsTcp,
|
|
255
|
+
"force",
|
|
256
|
+
"qr"
|
|
257
|
+
]);
|
|
258
|
+
function isKeyword(str) {
|
|
259
|
+
return KEYWORDS.has(str.toLowerCase());
|
|
260
|
+
}
|
|
1202
261
|
function parseUserAndDomain(str) {
|
|
1203
262
|
let token;
|
|
1204
263
|
let type;
|
|
1205
264
|
let server;
|
|
1206
265
|
let qrCode;
|
|
1207
|
-
|
|
266
|
+
let forceFlag;
|
|
267
|
+
if (!str) return { token, type, server, qrCode, forceFlag };
|
|
1208
268
|
if (str.includes("@")) {
|
|
1209
269
|
const [user, domain] = str.split("@", 2);
|
|
1210
270
|
if (domainRegex.test(domain)) {
|
|
271
|
+
let processKeyword2 = function(keyword) {
|
|
272
|
+
if ([TunnelType.Http, TunnelType.Tcp, TunnelType.Tls, TunnelType.Udp, TunnelType.TlsTcp].includes(keyword)) {
|
|
273
|
+
type = keyword;
|
|
274
|
+
} else if (keyword === "force") {
|
|
275
|
+
forceFlag = true;
|
|
276
|
+
} else if (keyword === "qr") {
|
|
277
|
+
qrCode = true;
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
var processKeyword = processKeyword2;
|
|
1211
281
|
server = domain;
|
|
1212
282
|
const parts = user.split("+");
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
283
|
+
if (parts.length === 0) {
|
|
284
|
+
return { token, type, server, qrCode, forceFlag };
|
|
285
|
+
}
|
|
286
|
+
const firstPart = parts[0];
|
|
287
|
+
if (!isKeyword(firstPart)) {
|
|
288
|
+
token = firstPart;
|
|
289
|
+
for (let i = 1; i < parts.length; i++) {
|
|
290
|
+
const part = parts[i].toLowerCase();
|
|
291
|
+
if (!isKeyword(part)) {
|
|
292
|
+
throw new Error(`Invalid user format: unexpected token '${part}' when keywords are expected.`);
|
|
293
|
+
}
|
|
294
|
+
processKeyword2(part);
|
|
295
|
+
}
|
|
296
|
+
} else {
|
|
297
|
+
for (const part of parts) {
|
|
298
|
+
const lowerPart = part.toLowerCase();
|
|
299
|
+
if (!isKeyword(lowerPart)) {
|
|
300
|
+
throw new Error(`Invalid user format: unexpected token '${lowerPart}' when keywords are expected.`);
|
|
301
|
+
}
|
|
302
|
+
processKeyword2(lowerPart);
|
|
1222
303
|
}
|
|
1223
304
|
}
|
|
1224
305
|
}
|
|
1225
306
|
} else if (domainRegex.test(str)) {
|
|
1226
307
|
server = str;
|
|
1227
308
|
}
|
|
1228
|
-
return { token, type, server, qrCode };
|
|
309
|
+
return { token, type, server, qrCode, forceFlag };
|
|
1229
310
|
}
|
|
1230
311
|
function parseUsers(positionalArgs, explicitToken) {
|
|
1231
312
|
let token;
|
|
@@ -1239,6 +320,8 @@ function parseUsers(positionalArgs, explicitToken) {
|
|
|
1239
320
|
if (parsed.server) server = parsed.server;
|
|
1240
321
|
if (parsed.type) type = parsed.type;
|
|
1241
322
|
if (parsed.token) token = parsed.token;
|
|
323
|
+
if (parsed.forceFlag) forceFlag = true;
|
|
324
|
+
if (parsed.qrCode) qrCode = true;
|
|
1242
325
|
}
|
|
1243
326
|
if (remaining.length > 0) {
|
|
1244
327
|
const first = remaining[0];
|
|
@@ -1246,19 +329,9 @@ function parseUsers(positionalArgs, explicitToken) {
|
|
|
1246
329
|
if (parsed.server) {
|
|
1247
330
|
server = parsed.server;
|
|
1248
331
|
if (parsed.type) type = parsed.type;
|
|
1249
|
-
if (parsed.token)
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
const tOnly = parts.filter((p) => p !== "force").join("+");
|
|
1253
|
-
if (tOnly) token = tOnly;
|
|
1254
|
-
if (parts.includes("force")) forceFlag = true;
|
|
1255
|
-
} else {
|
|
1256
|
-
token = parsed.token;
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
if (parsed.qrCode) {
|
|
1260
|
-
qrCode = true;
|
|
1261
|
-
}
|
|
332
|
+
if (parsed.token) token = parsed.token;
|
|
333
|
+
if (parsed.forceFlag) forceFlag = true;
|
|
334
|
+
if (parsed.qrCode) qrCode = true;
|
|
1262
335
|
remaining = remaining.slice(1);
|
|
1263
336
|
}
|
|
1264
337
|
}
|
|
@@ -1266,7 +339,7 @@ function parseUsers(positionalArgs, explicitToken) {
|
|
|
1266
339
|
}
|
|
1267
340
|
function parseType(finalConfig, values, inferredType) {
|
|
1268
341
|
const t = inferredType || values.type || finalConfig.tunnelType;
|
|
1269
|
-
if (t ===
|
|
342
|
+
if (t === TunnelType.Http || t === TunnelType.Tcp || t === TunnelType.Tls || t === TunnelType.Udp || t === TunnelType.TlsTcp) {
|
|
1270
343
|
finalConfig.tunnelType = [t];
|
|
1271
344
|
}
|
|
1272
345
|
}
|
|
@@ -1441,7 +514,7 @@ function parseLocalTunnelAddr(finalConfig, values) {
|
|
|
1441
514
|
const firstL = values.L[0];
|
|
1442
515
|
const parts = firstL.split(":");
|
|
1443
516
|
if (parts.length === 3) {
|
|
1444
|
-
const lp = parseInt(parts[
|
|
517
|
+
const lp = parseInt(parts[0], 10);
|
|
1445
518
|
if (!Number.isNaN(lp) && isValidPort(lp)) {
|
|
1446
519
|
finalConfig.webDebugger = `localhost:${lp}`;
|
|
1447
520
|
} else {
|
|
@@ -1473,10 +546,10 @@ function parseArgs(finalConfig, remainingPositionals) {
|
|
|
1473
546
|
}
|
|
1474
547
|
function storeJson(config, saveconf) {
|
|
1475
548
|
if (saveconf) {
|
|
1476
|
-
const
|
|
549
|
+
const path2 = saveconf;
|
|
1477
550
|
try {
|
|
1478
|
-
fs.writeFileSync(
|
|
1479
|
-
logger.info(`Configuration saved to ${
|
|
551
|
+
fs.writeFileSync(path2, JSON.stringify(config, null, 2), { encoding: "utf-8", flag: "w" });
|
|
552
|
+
logger.info(`Configuration saved to ${path2}`);
|
|
1480
553
|
} catch (err) {
|
|
1481
554
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1482
555
|
logger.error("Error loading configuration:", msg);
|
|
@@ -1486,7 +559,7 @@ function storeJson(config, saveconf) {
|
|
|
1486
559
|
function loadJsonConfig(config) {
|
|
1487
560
|
const configpath = config["conf"];
|
|
1488
561
|
if (typeof configpath === "string" && configpath.trim().length > 0) {
|
|
1489
|
-
const filepath =
|
|
562
|
+
const filepath = path.resolve(configpath);
|
|
1490
563
|
try {
|
|
1491
564
|
const data = fs.readFileSync(filepath, { encoding: "utf-8" });
|
|
1492
565
|
const json = JSON.parse(data);
|
|
@@ -1510,6 +583,20 @@ function parseServe(finalConfig, values) {
|
|
|
1510
583
|
finalConfig.serve = sv;
|
|
1511
584
|
return null;
|
|
1512
585
|
}
|
|
586
|
+
function parseAutoReconnect(finalConfig, values) {
|
|
587
|
+
const autoReconnectValue = values.autoreconnect;
|
|
588
|
+
if (typeof autoReconnectValue === "string") {
|
|
589
|
+
const trimmed = autoReconnectValue.trim().toLowerCase();
|
|
590
|
+
if (trimmed === "true" || trimmed === "") {
|
|
591
|
+
finalConfig.autoReconnect = true;
|
|
592
|
+
} else if (trimmed === "false") {
|
|
593
|
+
finalConfig.autoReconnect = false;
|
|
594
|
+
} else {
|
|
595
|
+
return new Error(`Invalid autoreconnect value: ${autoReconnectValue}. Use true or false.`);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
1513
600
|
async function buildFinalConfig(values, positionals) {
|
|
1514
601
|
let token;
|
|
1515
602
|
let server;
|
|
@@ -1534,10 +621,10 @@ async function buildFinalConfig(values, positionals) {
|
|
|
1534
621
|
configid: getRandomId(),
|
|
1535
622
|
token: token || (configFromFile?.token || (typeof values.token === "string" ? values.token : "")),
|
|
1536
623
|
serverAddress: server || (configFromFile?.serverAddress || defaultOptions.serverAddress),
|
|
1537
|
-
tunnelType: initialTunnel ? [initialTunnel] : configFromFile?.tunnelType || [
|
|
624
|
+
tunnelType: initialTunnel ? [initialTunnel] : configFromFile?.tunnelType || [TunnelType.Http],
|
|
1538
625
|
NoTUI: values.notui || (configFromFile?.NoTUI || false),
|
|
1539
626
|
qrCode: qrCode || (configFromFile?.qrCode || false),
|
|
1540
|
-
autoReconnect:
|
|
627
|
+
autoReconnect: configFromFile?.autoReconnect ? configFromFile.autoReconnect : defaultOptions.autoReconnect
|
|
1541
628
|
};
|
|
1542
629
|
parseType(finalConfig, values, type);
|
|
1543
630
|
parseToken(finalConfig, token || values.token);
|
|
@@ -1551,733 +638,14 @@ async function buildFinalConfig(values, positionals) {
|
|
|
1551
638
|
if (lErr instanceof Error) throw lErr;
|
|
1552
639
|
const serveErr = parseServe(finalConfig, values);
|
|
1553
640
|
if (serveErr instanceof Error) throw serveErr;
|
|
641
|
+
const autoReconnectErr = parseAutoReconnect(finalConfig, values);
|
|
642
|
+
if (autoReconnectErr instanceof Error) throw autoReconnectErr;
|
|
1554
643
|
if (forceFlag) finalConfig.force = true;
|
|
1555
644
|
parseArgs(finalConfig, remainingPositionals);
|
|
1556
645
|
storeJson(finalConfig, saveconf);
|
|
1557
646
|
return finalConfig;
|
|
1558
647
|
}
|
|
1559
648
|
|
|
1560
|
-
// src/remote_management/remoteManagement.ts
|
|
1561
|
-
import WebSocket from "ws";
|
|
1562
|
-
|
|
1563
|
-
// src/types.ts
|
|
1564
|
-
var ErrorCode = {
|
|
1565
|
-
InvalidRequestMethodError: "INVALID_REQUEST_METHOD",
|
|
1566
|
-
InvalidRequestBodyError: "COULD_NOT_READ_BODY",
|
|
1567
|
-
InternalServerError: "INTERNAL_SERVER_ERROR",
|
|
1568
|
-
InvalidBodyFormatError: "INVALID_DATA_FORMAT",
|
|
1569
|
-
ErrorStartingTunnel: "ERROR_STARTING_TUNNEL",
|
|
1570
|
-
TunnelNotFound: "TUNNEL_WITH_ID_OR_CONFIG_ID_NOT_FOUND",
|
|
1571
|
-
TunnelAlreadyRunningError: "TUNNEL_WITH_ID_OR_CONFIG_ID_ALREADY_RUNNING",
|
|
1572
|
-
WebsocketUpgradeFailError: "WEBSOCKET_UPGRADE_FAILED",
|
|
1573
|
-
RemoteManagementAlreadyRunning: "REMOTE_MANAGEMENT_ALREADY_RUNNING",
|
|
1574
|
-
RemoteManagementNotRunning: "REMOTE_MANAGEMENT_NOT_RUNNING",
|
|
1575
|
-
RemoteManagementDeserializationFailed: "REMOTE_MANAGEMENT_DESERIALIZATION_FAILED"
|
|
1576
|
-
};
|
|
1577
|
-
function isErrorResponse(obj) {
|
|
1578
|
-
return typeof obj === "object" && obj !== null && "code" in obj && "message" in obj && typeof obj.message === "string" && Object.values(ErrorCode).includes(obj.code);
|
|
1579
|
-
}
|
|
1580
|
-
function newErrorResponse(codeOrError, message) {
|
|
1581
|
-
if (typeof codeOrError === "object") {
|
|
1582
|
-
return codeOrError;
|
|
1583
|
-
}
|
|
1584
|
-
return {
|
|
1585
|
-
code: codeOrError,
|
|
1586
|
-
message
|
|
1587
|
-
};
|
|
1588
|
-
}
|
|
1589
|
-
function NewResponseObject(data) {
|
|
1590
|
-
const encoder = new TextEncoder();
|
|
1591
|
-
const bytes = encoder.encode(JSON.stringify(data));
|
|
1592
|
-
return {
|
|
1593
|
-
response: bytes,
|
|
1594
|
-
requestid: "",
|
|
1595
|
-
command: "",
|
|
1596
|
-
error: false,
|
|
1597
|
-
errorresponse: {}
|
|
1598
|
-
};
|
|
1599
|
-
}
|
|
1600
|
-
function NewErrorResponseObject(errorResponse) {
|
|
1601
|
-
return {
|
|
1602
|
-
response: new Uint8Array(),
|
|
1603
|
-
requestid: "",
|
|
1604
|
-
command: "",
|
|
1605
|
-
error: true,
|
|
1606
|
-
errorresponse: errorResponse
|
|
1607
|
-
};
|
|
1608
|
-
}
|
|
1609
|
-
function newStatus(tunnelState, errorCode, errorMsg) {
|
|
1610
|
-
let assignedState = tunnelState;
|
|
1611
|
-
if (tunnelState === "live" /* Live */) {
|
|
1612
|
-
assignedState = "running" /* Running */;
|
|
1613
|
-
} else if (tunnelState === "idle" /* New */) {
|
|
1614
|
-
assignedState = "idle" /* New */;
|
|
1615
|
-
} else if (tunnelState === "closed" /* Closed */) {
|
|
1616
|
-
assignedState = "exited" /* Exited */;
|
|
1617
|
-
}
|
|
1618
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1619
|
-
return {
|
|
1620
|
-
state: assignedState,
|
|
1621
|
-
errorcode: errorCode,
|
|
1622
|
-
errormsg: errorMsg,
|
|
1623
|
-
createdtimestamp: now,
|
|
1624
|
-
starttimestamp: now,
|
|
1625
|
-
endtimestamp: now,
|
|
1626
|
-
warnings: []
|
|
1627
|
-
};
|
|
1628
|
-
}
|
|
1629
|
-
function newStats() {
|
|
1630
|
-
return {
|
|
1631
|
-
numLiveConnections: 0,
|
|
1632
|
-
numTotalConnections: 0,
|
|
1633
|
-
numTotalReqBytes: 0,
|
|
1634
|
-
numTotalResBytes: 0,
|
|
1635
|
-
numTotalTxBytes: 0,
|
|
1636
|
-
elapsedTime: 0
|
|
1637
|
-
};
|
|
1638
|
-
}
|
|
1639
|
-
var RemoteManagementStatus = {
|
|
1640
|
-
Connecting: "CONNECTING",
|
|
1641
|
-
Disconnecting: "DISCONNECTING",
|
|
1642
|
-
Reconnecting: "RECONNECTING",
|
|
1643
|
-
Running: "RUNNING",
|
|
1644
|
-
NotRunning: "NOT_RUNNING",
|
|
1645
|
-
Error: "ERROR"
|
|
1646
|
-
};
|
|
1647
|
-
|
|
1648
|
-
// src/remote_management/remote_schema.ts
|
|
1649
|
-
import { TunnelType as TunnelType3 } from "@pinggy/pinggy";
|
|
1650
|
-
import { z } from "zod";
|
|
1651
|
-
var HeaderModificationSchema = z.object({
|
|
1652
|
-
key: z.string(),
|
|
1653
|
-
value: z.array(z.string()).optional(),
|
|
1654
|
-
type: z.enum(["add", "remove", "update"])
|
|
1655
|
-
});
|
|
1656
|
-
var AdditionalForwardingSchema = z.object({
|
|
1657
|
-
remoteDomain: z.string().optional(),
|
|
1658
|
-
remotePort: z.number().optional(),
|
|
1659
|
-
localDomain: z.string(),
|
|
1660
|
-
localPort: z.number()
|
|
1661
|
-
});
|
|
1662
|
-
var TunnelConfigSchema = z.object({
|
|
1663
|
-
allowPreflight: z.boolean().optional(),
|
|
1664
|
-
// primary key
|
|
1665
|
-
allowpreflight: z.boolean().optional(),
|
|
1666
|
-
// legacy key
|
|
1667
|
-
autoreconnect: z.boolean(),
|
|
1668
|
-
basicauth: z.array(z.object({ username: z.string(), password: z.string() })).nullable(),
|
|
1669
|
-
bearerauth: z.string().nullable(),
|
|
1670
|
-
configid: z.string(),
|
|
1671
|
-
configname: z.string(),
|
|
1672
|
-
greetmsg: z.string().optional(),
|
|
1673
|
-
force: z.boolean(),
|
|
1674
|
-
forwardedhost: z.string(),
|
|
1675
|
-
fullRequestUrl: z.boolean(),
|
|
1676
|
-
headermodification: z.array(HeaderModificationSchema),
|
|
1677
|
-
httpsOnly: z.boolean(),
|
|
1678
|
-
internalwebdebuggerport: z.number(),
|
|
1679
|
-
ipwhitelist: z.array(z.string()).nullable(),
|
|
1680
|
-
localport: z.number(),
|
|
1681
|
-
localsservertls: z.union([z.boolean(), z.string()]),
|
|
1682
|
-
localservertlssni: z.string().nullable(),
|
|
1683
|
-
regioncode: z.string(),
|
|
1684
|
-
noReverseProxy: z.boolean(),
|
|
1685
|
-
serveraddress: z.string(),
|
|
1686
|
-
serverport: z.number(),
|
|
1687
|
-
statusCheckInterval: z.number(),
|
|
1688
|
-
token: z.string(),
|
|
1689
|
-
tunnelTimeout: z.number(),
|
|
1690
|
-
type: z.enum([
|
|
1691
|
-
TunnelType3.Http,
|
|
1692
|
-
TunnelType3.Tcp,
|
|
1693
|
-
TunnelType3.Udp,
|
|
1694
|
-
TunnelType3.Tls,
|
|
1695
|
-
TunnelType3.TlsTcp
|
|
1696
|
-
]),
|
|
1697
|
-
webdebuggerport: z.number(),
|
|
1698
|
-
xff: z.string(),
|
|
1699
|
-
additionalForwarding: z.array(AdditionalForwardingSchema).optional(),
|
|
1700
|
-
serve: z.string().optional()
|
|
1701
|
-
}).superRefine((data, ctx) => {
|
|
1702
|
-
if (data.allowPreflight === void 0 && data.allowpreflight === void 0) {
|
|
1703
|
-
ctx.addIssue({
|
|
1704
|
-
code: "custom",
|
|
1705
|
-
message: "Either allowPreflight or allowpreflight is required",
|
|
1706
|
-
path: ["allowPreflight"]
|
|
1707
|
-
});
|
|
1708
|
-
}
|
|
1709
|
-
}).transform((data) => ({
|
|
1710
|
-
...data,
|
|
1711
|
-
allowPreflight: data.allowPreflight ?? data.allowpreflight,
|
|
1712
|
-
allowpreflight: data.allowPreflight ?? data.allowpreflight
|
|
1713
|
-
}));
|
|
1714
|
-
var StartSchema = z.object({
|
|
1715
|
-
tunnelID: z.string().nullable().optional(),
|
|
1716
|
-
tunnelConfig: TunnelConfigSchema
|
|
1717
|
-
});
|
|
1718
|
-
var StopSchema = z.object({
|
|
1719
|
-
tunnelID: z.string().min(1)
|
|
1720
|
-
});
|
|
1721
|
-
var GetSchema = StopSchema;
|
|
1722
|
-
var RestartSchema = StopSchema;
|
|
1723
|
-
var UpdateConfigSchema = z.object({
|
|
1724
|
-
tunnelConfig: TunnelConfigSchema
|
|
1725
|
-
});
|
|
1726
|
-
function tunnelConfigToPinggyOptions(config) {
|
|
1727
|
-
return {
|
|
1728
|
-
token: config.token || "",
|
|
1729
|
-
serverAddress: config.serveraddress || "free.pinggy.io",
|
|
1730
|
-
forwarding: `${config.forwardedhost || "localhost"}:${config.localport}`,
|
|
1731
|
-
webDebugger: config.webdebuggerport ? `localhost:${config.webdebuggerport}` : "",
|
|
1732
|
-
ipWhitelist: config.ipwhitelist || [],
|
|
1733
|
-
basicAuth: config.basicauth ? config.basicauth : [],
|
|
1734
|
-
bearerTokenAuth: config.bearerauth ? [config.bearerauth] : [],
|
|
1735
|
-
headerModification: config.headermodification,
|
|
1736
|
-
xForwardedFor: !!config.xff,
|
|
1737
|
-
httpsOnly: config.httpsOnly,
|
|
1738
|
-
originalRequestUrl: config.fullRequestUrl,
|
|
1739
|
-
allowPreflight: config.allowPreflight,
|
|
1740
|
-
reverseProxy: config.noReverseProxy,
|
|
1741
|
-
force: config.force,
|
|
1742
|
-
autoReconnect: config.autoreconnect,
|
|
1743
|
-
optional: {
|
|
1744
|
-
sniServerName: config.localservertlssni || ""
|
|
1745
|
-
}
|
|
1746
|
-
};
|
|
1747
|
-
}
|
|
1748
|
-
function pinggyOptionsToTunnelConfig(opts, configid, configName, localserverTls, greetMsg, additionalForwarding, serve) {
|
|
1749
|
-
const forwarding = Array.isArray(opts.forwarding) ? String(opts.forwarding[0].address).replace("//", "").replace(/\/$/, "") : String(opts.forwarding).replace("//", "").replace(/\/$/, "");
|
|
1750
|
-
const parsedForwardedHost = forwarding.split(":").length == 3 ? forwarding.split(":")[1] : forwarding.split(":")[0];
|
|
1751
|
-
const parsedLocalPort = forwarding.split(":").length == 3 ? parseInt(forwarding.split(":")[2], 10) : parseInt(forwarding.split(":")[1], 10);
|
|
1752
|
-
const tunnelType = (Array.isArray(opts.forwarding) ? opts.forwarding[0]?.type : void 0) ?? TunnelType3.Http;
|
|
1753
|
-
const parsedTokens = opts.bearerTokenAuth ? Array.isArray(opts.bearerTokenAuth) ? opts.bearerTokenAuth : JSON.parse(opts.bearerTokenAuth) : [];
|
|
1754
|
-
return {
|
|
1755
|
-
allowPreflight: opts.allowPreflight ?? false,
|
|
1756
|
-
allowpreflight: opts.allowPreflight ?? false,
|
|
1757
|
-
autoreconnect: opts.autoReconnect ?? false,
|
|
1758
|
-
basicauth: opts.basicAuth && Object.keys(opts.basicAuth).length ? opts.basicAuth : null,
|
|
1759
|
-
bearerauth: parsedTokens.length ? parsedTokens.join(",") : null,
|
|
1760
|
-
configid,
|
|
1761
|
-
configname: configName,
|
|
1762
|
-
greetmsg: greetMsg || "",
|
|
1763
|
-
force: opts.force ?? false,
|
|
1764
|
-
forwardedhost: parsedForwardedHost || "localhost",
|
|
1765
|
-
fullRequestUrl: opts.originalRequestUrl ?? false,
|
|
1766
|
-
headermodification: opts.headerModification || [],
|
|
1767
|
-
//structured list
|
|
1768
|
-
httpsOnly: opts.httpsOnly ?? false,
|
|
1769
|
-
internalwebdebuggerport: 0,
|
|
1770
|
-
ipwhitelist: opts.ipWhitelist ? Array.isArray(opts.ipWhitelist) ? opts.ipWhitelist : JSON.parse(opts.ipWhitelist) : null,
|
|
1771
|
-
localport: parsedLocalPort || 0,
|
|
1772
|
-
localservertlssni: null,
|
|
1773
|
-
regioncode: "",
|
|
1774
|
-
noReverseProxy: opts.reverseProxy ?? false,
|
|
1775
|
-
serveraddress: opts.serverAddress || "free.pinggy.io",
|
|
1776
|
-
serverport: 0,
|
|
1777
|
-
statusCheckInterval: 0,
|
|
1778
|
-
token: opts.token || "",
|
|
1779
|
-
tunnelTimeout: 0,
|
|
1780
|
-
type: tunnelType,
|
|
1781
|
-
webdebuggerport: Number(opts.webDebugger?.split(":")[0]) || 0,
|
|
1782
|
-
xff: opts.xForwardedFor ? "1" : "",
|
|
1783
|
-
localsservertls: localserverTls || false,
|
|
1784
|
-
additionalForwarding: additionalForwarding || [],
|
|
1785
|
-
serve: serve || ""
|
|
1786
|
-
};
|
|
1787
|
-
}
|
|
1788
|
-
|
|
1789
|
-
// src/remote_management/handler.ts
|
|
1790
|
-
import { TunnelType as TunnelType4 } from "@pinggy/pinggy";
|
|
1791
|
-
var TunnelOperations = class {
|
|
1792
|
-
constructor() {
|
|
1793
|
-
this.tunnelManager = TunnelManager.getInstance();
|
|
1794
|
-
}
|
|
1795
|
-
buildStatus(tunnelId, state, errorCode) {
|
|
1796
|
-
const status = newStatus(state, errorCode, "");
|
|
1797
|
-
try {
|
|
1798
|
-
const managed = this.tunnelManager.getManagedTunnel("", tunnelId);
|
|
1799
|
-
if (managed) {
|
|
1800
|
-
status.createdtimestamp = managed.createdAt || "";
|
|
1801
|
-
status.starttimestamp = managed.startedAt || "";
|
|
1802
|
-
status.endtimestamp = managed.stoppedAt || "";
|
|
1803
|
-
}
|
|
1804
|
-
} catch (e) {
|
|
1805
|
-
}
|
|
1806
|
-
return status;
|
|
1807
|
-
}
|
|
1808
|
-
// --- Helper to construct TunnelResponse ---
|
|
1809
|
-
async buildTunnelResponse(tunnelid, tunnelConfig, configid, tunnelName, additionalForwarding, serve) {
|
|
1810
|
-
const [status, stats, tlsInfo, greetMsg, remoteurls] = await Promise.all([
|
|
1811
|
-
this.tunnelManager.getTunnelStatus(tunnelid),
|
|
1812
|
-
this.tunnelManager.getLatestTunnelStats(tunnelid) || newStats(),
|
|
1813
|
-
this.tunnelManager.getLocalserverTlsInfo(tunnelid),
|
|
1814
|
-
this.tunnelManager.getTunnelGreetMessage(tunnelid),
|
|
1815
|
-
this.tunnelManager.getTunnelUrls(tunnelid)
|
|
1816
|
-
]);
|
|
1817
|
-
return {
|
|
1818
|
-
tunnelid,
|
|
1819
|
-
remoteurls,
|
|
1820
|
-
tunnelconfig: pinggyOptionsToTunnelConfig(tunnelConfig, configid, tunnelName, tlsInfo, greetMsg, additionalForwarding),
|
|
1821
|
-
status: this.buildStatus(tunnelid, status, "" /* NoError */),
|
|
1822
|
-
stats
|
|
1823
|
-
};
|
|
1824
|
-
}
|
|
1825
|
-
error(code, err, fallback) {
|
|
1826
|
-
return newErrorResponse({
|
|
1827
|
-
code,
|
|
1828
|
-
message: err instanceof Error ? err.message : fallback
|
|
1829
|
-
});
|
|
1830
|
-
}
|
|
1831
|
-
// --- Operations ---
|
|
1832
|
-
async handleStart(config) {
|
|
1833
|
-
try {
|
|
1834
|
-
const opts = tunnelConfigToPinggyOptions(config);
|
|
1835
|
-
const additionalForwardingParsed = config.additionalForwarding || [];
|
|
1836
|
-
const { tunnelid, instance, tunnelName, additionalForwarding, serve } = await this.tunnelManager.createTunnel({
|
|
1837
|
-
...opts,
|
|
1838
|
-
tunnelType: Array.isArray(config.type) ? config.type : config.type ? [config.type] : [TunnelType4.Http],
|
|
1839
|
-
// Temporary fix in future we will not use this field.
|
|
1840
|
-
configid: config.configid,
|
|
1841
|
-
tunnelName: config.configname,
|
|
1842
|
-
additionalForwarding: additionalForwardingParsed,
|
|
1843
|
-
serve: config.serve
|
|
1844
|
-
});
|
|
1845
|
-
this.tunnelManager.startTunnel(tunnelid);
|
|
1846
|
-
const tunnelPconfig = await this.tunnelManager.getTunnelConfig("", tunnelid);
|
|
1847
|
-
const resp = this.buildTunnelResponse(tunnelid, tunnelPconfig, config.configid, tunnelName, additionalForwarding, serve);
|
|
1848
|
-
return resp;
|
|
1849
|
-
} catch (err) {
|
|
1850
|
-
return this.error(ErrorCode.ErrorStartingTunnel, err, "Unknown error occurred while starting tunnel");
|
|
1851
|
-
}
|
|
1852
|
-
}
|
|
1853
|
-
async handleUpdateConfig(config) {
|
|
1854
|
-
try {
|
|
1855
|
-
const opts = tunnelConfigToPinggyOptions(config);
|
|
1856
|
-
const tunnel = await this.tunnelManager.updateConfig({
|
|
1857
|
-
...opts,
|
|
1858
|
-
tunnelType: Array.isArray(config.type) ? config.type : config.type ? [config.type] : [TunnelType4.Http],
|
|
1859
|
-
// // Temporary fix in future we will not use this field.
|
|
1860
|
-
configid: config.configid,
|
|
1861
|
-
tunnelName: config.configname,
|
|
1862
|
-
additionalForwarding: config.additionalForwarding || [],
|
|
1863
|
-
serve: config.serve
|
|
1864
|
-
});
|
|
1865
|
-
if (!tunnel.instance || !tunnel.tunnelConfig)
|
|
1866
|
-
throw new Error("Invalid tunnel state after configuration update");
|
|
1867
|
-
return this.buildTunnelResponse(tunnel.tunnelid, tunnel.tunnelConfig, config.configid, tunnel.tunnelName, tunnel.additionalForwarding, tunnel.serve);
|
|
1868
|
-
} catch (err) {
|
|
1869
|
-
return this.error(ErrorCode.InternalServerError, err, "Failed to update tunnel configuration");
|
|
1870
|
-
}
|
|
1871
|
-
}
|
|
1872
|
-
async handleList() {
|
|
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 pinggyOptions = status !== "closed" /* Closed */ && status !== "exited" /* Exited */ ? await this.tunnelManager.getTunnelConfig("", t.tunnelid) : t.tunnelConfig;
|
|
1887
|
-
const tunnelConfig = pinggyOptionsToTunnelConfig(pinggyOptions, t.configid, t.tunnelName, tlsInfo, greetMsg, t.additionalForwarding, t.serve);
|
|
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
|
-
};
|
|
1895
|
-
})
|
|
1896
|
-
);
|
|
1897
|
-
} catch (err) {
|
|
1898
|
-
return this.error(ErrorCode.InternalServerError, err, "Failed to list tunnels");
|
|
1899
|
-
}
|
|
1900
|
-
}
|
|
1901
|
-
async handleStop(tunnelid) {
|
|
1902
|
-
try {
|
|
1903
|
-
const { configid } = this.tunnelManager.stopTunnel(tunnelid);
|
|
1904
|
-
const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
|
|
1905
|
-
if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
|
|
1906
|
-
return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, configid, managed.tunnelName, managed.additionalForwarding, managed.serve);
|
|
1907
|
-
} catch (err) {
|
|
1908
|
-
return this.error(ErrorCode.TunnelNotFound, err, "Failed to stop tunnel");
|
|
1909
|
-
}
|
|
1910
|
-
}
|
|
1911
|
-
async handleGet(tunnelid) {
|
|
1912
|
-
try {
|
|
1913
|
-
const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
|
|
1914
|
-
if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
|
|
1915
|
-
return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, managed.configid, managed.tunnelName, managed.additionalForwarding, managed.serve);
|
|
1916
|
-
} catch (err) {
|
|
1917
|
-
return this.error(ErrorCode.TunnelNotFound, err, "Failed to get tunnel information");
|
|
1918
|
-
}
|
|
1919
|
-
}
|
|
1920
|
-
async handleRestart(tunnelid) {
|
|
1921
|
-
try {
|
|
1922
|
-
await this.tunnelManager.restartTunnel(tunnelid);
|
|
1923
|
-
const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
|
|
1924
|
-
if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
|
|
1925
|
-
return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, managed.configid, managed.tunnelName, managed.additionalForwarding, managed.serve);
|
|
1926
|
-
} catch (err) {
|
|
1927
|
-
return this.error(ErrorCode.TunnelNotFound, err, "Failed to restart tunnel");
|
|
1928
|
-
}
|
|
1929
|
-
}
|
|
1930
|
-
handleRegisterStatsListener(tunnelid, listener) {
|
|
1931
|
-
this.tunnelManager.registerStatsListener(tunnelid, listener);
|
|
1932
|
-
}
|
|
1933
|
-
handleUnregisterStatsListener(tunnelid, listnerId) {
|
|
1934
|
-
this.tunnelManager.deregisterStatsListener(tunnelid, listnerId);
|
|
1935
|
-
}
|
|
1936
|
-
handleGetTunnelStats(tunnelid) {
|
|
1937
|
-
try {
|
|
1938
|
-
const stats = this.tunnelManager.getTunnelStats(tunnelid);
|
|
1939
|
-
if (!stats) {
|
|
1940
|
-
return [newStats()];
|
|
1941
|
-
}
|
|
1942
|
-
return stats;
|
|
1943
|
-
} catch (err) {
|
|
1944
|
-
return this.error(ErrorCode.TunnelNotFound, err, "Failed to get tunnel stats");
|
|
1945
|
-
}
|
|
1946
|
-
}
|
|
1947
|
-
handleRegisterDisconnectListener(tunnelid, listener) {
|
|
1948
|
-
this.tunnelManager.registerDisconnectListener(tunnelid, listener);
|
|
1949
|
-
}
|
|
1950
|
-
handleRemoveStoppedTunnelByConfigId(configId) {
|
|
1951
|
-
try {
|
|
1952
|
-
return this.tunnelManager.removeStoppedTunnelByConfigId(configId);
|
|
1953
|
-
} catch (err) {
|
|
1954
|
-
return this.error(ErrorCode.InternalServerError, err, "Failed to remove stopped tunnel by configId");
|
|
1955
|
-
}
|
|
1956
|
-
}
|
|
1957
|
-
handleRemoveStoppedTunnelByTunnelId(tunnelId) {
|
|
1958
|
-
try {
|
|
1959
|
-
return this.tunnelManager.removeStoppedTunnelByTunnelId(tunnelId);
|
|
1960
|
-
} catch (err) {
|
|
1961
|
-
return this.error(ErrorCode.InternalServerError, err, "Failed to remove stopped tunnel by tunnelId");
|
|
1962
|
-
}
|
|
1963
|
-
}
|
|
1964
|
-
};
|
|
1965
|
-
|
|
1966
|
-
// src/remote_management/websocket_handlers.ts
|
|
1967
|
-
import z2 from "zod";
|
|
1968
|
-
var WebSocketCommandHandler = class {
|
|
1969
|
-
constructor() {
|
|
1970
|
-
this.tunnelHandler = new TunnelOperations();
|
|
1971
|
-
}
|
|
1972
|
-
safeParse(text) {
|
|
1973
|
-
if (!text) return void 0;
|
|
1974
|
-
try {
|
|
1975
|
-
return JSON.parse(text);
|
|
1976
|
-
} catch (e) {
|
|
1977
|
-
logger.warn("Invalid JSON payload", { error: String(e), text });
|
|
1978
|
-
return void 0;
|
|
1979
|
-
}
|
|
1980
|
-
}
|
|
1981
|
-
sendResponse(ws, resp) {
|
|
1982
|
-
const payload = {
|
|
1983
|
-
...resp,
|
|
1984
|
-
response: Buffer.from(resp.response || []).toString("base64")
|
|
1985
|
-
};
|
|
1986
|
-
ws.send(JSON.stringify(payload));
|
|
1987
|
-
}
|
|
1988
|
-
sendError(ws, req, message, code = ErrorCode.InternalServerError) {
|
|
1989
|
-
const resp = NewErrorResponseObject({ code, message });
|
|
1990
|
-
resp.command = req.command || "";
|
|
1991
|
-
resp.requestid = req.requestid || "";
|
|
1992
|
-
this.sendResponse(ws, resp);
|
|
1993
|
-
}
|
|
1994
|
-
async handleStartReq(req, raw) {
|
|
1995
|
-
const dc = StartSchema.parse(raw);
|
|
1996
|
-
printer_default.info("Starting tunnel with config name: " + dc.tunnelConfig.configname);
|
|
1997
|
-
const result = await this.tunnelHandler.handleStart(dc.tunnelConfig);
|
|
1998
|
-
return this.wrapResponse(result, req);
|
|
1999
|
-
}
|
|
2000
|
-
async handleStopReq(req, raw) {
|
|
2001
|
-
const dc = StopSchema.parse(raw);
|
|
2002
|
-
printer_default.info("Stopping tunnel with ID: " + dc.tunnelID);
|
|
2003
|
-
const result = await this.tunnelHandler.handleStop(dc.tunnelID);
|
|
2004
|
-
return this.wrapResponse(result, req);
|
|
2005
|
-
}
|
|
2006
|
-
async handleGetReq(req, raw) {
|
|
2007
|
-
const dc = GetSchema.parse(raw);
|
|
2008
|
-
const result = await this.tunnelHandler.handleGet(dc.tunnelID);
|
|
2009
|
-
return this.wrapResponse(result, req);
|
|
2010
|
-
}
|
|
2011
|
-
async handleRestartReq(req, raw) {
|
|
2012
|
-
const dc = RestartSchema.parse(raw);
|
|
2013
|
-
const result = await this.tunnelHandler.handleRestart(dc.tunnelID);
|
|
2014
|
-
return this.wrapResponse(result, req);
|
|
2015
|
-
}
|
|
2016
|
-
async handleUpdateConfigReq(req, raw) {
|
|
2017
|
-
const dc = UpdateConfigSchema.parse(raw);
|
|
2018
|
-
const result = await this.tunnelHandler.handleUpdateConfig(dc.tunnelConfig);
|
|
2019
|
-
return this.wrapResponse(result, req);
|
|
2020
|
-
}
|
|
2021
|
-
async handleListReq(req) {
|
|
2022
|
-
const result = await this.tunnelHandler.handleList();
|
|
2023
|
-
return this.wrapResponse(result, req);
|
|
2024
|
-
}
|
|
2025
|
-
wrapResponse(result, req) {
|
|
2026
|
-
if (isErrorResponse(result)) {
|
|
2027
|
-
const errResp = NewErrorResponseObject(result);
|
|
2028
|
-
errResp.command = req.command;
|
|
2029
|
-
errResp.requestid = req.requestid;
|
|
2030
|
-
return errResp;
|
|
2031
|
-
}
|
|
2032
|
-
const finalResult = JSON.parse(JSON.stringify(result));
|
|
2033
|
-
if (Array.isArray(finalResult)) {
|
|
2034
|
-
finalResult.forEach((item) => {
|
|
2035
|
-
if (item?.tunnelconfig) {
|
|
2036
|
-
delete item.tunnelconfig.allowPreflight;
|
|
2037
|
-
}
|
|
2038
|
-
});
|
|
2039
|
-
} else if (finalResult?.tunnelconfig) {
|
|
2040
|
-
delete finalResult.tunnelconfig.allowPreflight;
|
|
2041
|
-
}
|
|
2042
|
-
const respObj = NewResponseObject(finalResult);
|
|
2043
|
-
respObj.command = req.command;
|
|
2044
|
-
respObj.requestid = req.requestid;
|
|
2045
|
-
return respObj;
|
|
2046
|
-
}
|
|
2047
|
-
async handle(ws, req) {
|
|
2048
|
-
const cmd = (req.command || "").toLowerCase();
|
|
2049
|
-
const raw = this.safeParse(req.data);
|
|
2050
|
-
try {
|
|
2051
|
-
let response;
|
|
2052
|
-
switch (cmd) {
|
|
2053
|
-
case "start": {
|
|
2054
|
-
response = await this.handleStartReq(req, raw);
|
|
2055
|
-
break;
|
|
2056
|
-
}
|
|
2057
|
-
case "stop": {
|
|
2058
|
-
response = await this.handleStopReq(req, raw);
|
|
2059
|
-
break;
|
|
2060
|
-
}
|
|
2061
|
-
case "get": {
|
|
2062
|
-
response = await this.handleGetReq(req, raw);
|
|
2063
|
-
break;
|
|
2064
|
-
}
|
|
2065
|
-
case "restart": {
|
|
2066
|
-
response = await this.handleRestartReq(req, raw);
|
|
2067
|
-
break;
|
|
2068
|
-
}
|
|
2069
|
-
case "updateconfig": {
|
|
2070
|
-
response = await this.handleUpdateConfigReq(req, raw);
|
|
2071
|
-
break;
|
|
2072
|
-
}
|
|
2073
|
-
case "list": {
|
|
2074
|
-
response = await this.handleListReq(req);
|
|
2075
|
-
break;
|
|
2076
|
-
}
|
|
2077
|
-
default:
|
|
2078
|
-
if (typeof req.command === "string") {
|
|
2079
|
-
logger.warn("Unknown command", { command: req.command });
|
|
2080
|
-
}
|
|
2081
|
-
return this.sendError(ws, req, "Invalid command");
|
|
2082
|
-
}
|
|
2083
|
-
logger.debug("Sending response", { command: response.command, requestid: response.requestid });
|
|
2084
|
-
this.sendResponse(ws, response);
|
|
2085
|
-
} catch (e) {
|
|
2086
|
-
if (e instanceof z2.ZodError) {
|
|
2087
|
-
logger.warn("Validation failed", { cmd, issues: e.issues });
|
|
2088
|
-
return this.sendError(ws, req, "Invalid request data", ErrorCode.InvalidBodyFormatError);
|
|
2089
|
-
}
|
|
2090
|
-
logger.error("Error handling command", { cmd, error: String(e) });
|
|
2091
|
-
return this.sendError(ws, req, e?.message || "Internal error");
|
|
2092
|
-
}
|
|
2093
|
-
}
|
|
2094
|
-
};
|
|
2095
|
-
function handleConnectionStatusMessage(firstMessage) {
|
|
2096
|
-
try {
|
|
2097
|
-
const text = typeof firstMessage === "string" ? firstMessage : firstMessage.toString();
|
|
2098
|
-
const cs = JSON.parse(text);
|
|
2099
|
-
if (!cs.success) {
|
|
2100
|
-
const msg = cs.error_msg || "Connection failed";
|
|
2101
|
-
printer_default.warn(`Connection failed: ${msg}`);
|
|
2102
|
-
logger.warn("Remote management connection failed", { error_code: cs.error_code, error_msg: msg });
|
|
2103
|
-
return false;
|
|
2104
|
-
}
|
|
2105
|
-
return true;
|
|
2106
|
-
} catch (e) {
|
|
2107
|
-
logger.warn("Failed to parse connection status message", { error: String(e) });
|
|
2108
|
-
return true;
|
|
2109
|
-
}
|
|
2110
|
-
}
|
|
2111
|
-
|
|
2112
|
-
// src/remote_management/remoteManagement.ts
|
|
2113
|
-
var RECONNECT_SLEEP_MS = 5e3;
|
|
2114
|
-
var PING_INTERVAL_MS = 3e4;
|
|
2115
|
-
var _remoteManagementState = {
|
|
2116
|
-
status: "NOT_RUNNING",
|
|
2117
|
-
errorMessage: ""
|
|
2118
|
-
};
|
|
2119
|
-
var _stopRequested = false;
|
|
2120
|
-
var currentWs = null;
|
|
2121
|
-
function buildRemoteManagementWsUrl(manage) {
|
|
2122
|
-
let baseUrl = (manage || "dashboard.pinggy.io").trim();
|
|
2123
|
-
if (!(baseUrl.startsWith("ws://") || baseUrl.startsWith("wss://"))) {
|
|
2124
|
-
baseUrl = "wss://" + baseUrl;
|
|
2125
|
-
}
|
|
2126
|
-
const trimmed = baseUrl.replace(/\/$/, "");
|
|
2127
|
-
return `${trimmed}/backend/api/v1/remote-management/connect`;
|
|
2128
|
-
}
|
|
2129
|
-
function extractHostname(u) {
|
|
2130
|
-
try {
|
|
2131
|
-
const url = new URL(u);
|
|
2132
|
-
return url.host;
|
|
2133
|
-
} catch {
|
|
2134
|
-
return u;
|
|
2135
|
-
}
|
|
2136
|
-
}
|
|
2137
|
-
function sleep(ms) {
|
|
2138
|
-
return new Promise((res) => setTimeout(res, ms));
|
|
2139
|
-
}
|
|
2140
|
-
async function parseRemoteManagement(values) {
|
|
2141
|
-
const rmToken = values["remote-management"];
|
|
2142
|
-
if (typeof rmToken === "string" && rmToken.trim().length > 0) {
|
|
2143
|
-
const manageHost = values["manage"];
|
|
2144
|
-
try {
|
|
2145
|
-
await initiateRemoteManagement(rmToken, manageHost);
|
|
2146
|
-
return { ok: true };
|
|
2147
|
-
} catch (e) {
|
|
2148
|
-
logger.error("Failed to initiate remote management:", e);
|
|
2149
|
-
return { ok: false, error: e };
|
|
2150
|
-
}
|
|
2151
|
-
}
|
|
2152
|
-
}
|
|
2153
|
-
async function initiateRemoteManagement(token, manage) {
|
|
2154
|
-
if (!token || token.trim().length === 0) {
|
|
2155
|
-
throw new Error("Remote management token is required (use --remote-management <TOKEN>)");
|
|
2156
|
-
}
|
|
2157
|
-
const wsUrl = buildRemoteManagementWsUrl(manage);
|
|
2158
|
-
const wsHost = extractHostname(wsUrl);
|
|
2159
|
-
logger.info("Remote management mode enabled.");
|
|
2160
|
-
_stopRequested = false;
|
|
2161
|
-
const sigintHandler = () => {
|
|
2162
|
-
_stopRequested = true;
|
|
2163
|
-
};
|
|
2164
|
-
process.once("SIGINT", sigintHandler);
|
|
2165
|
-
const logConnecting = () => {
|
|
2166
|
-
printer_default.print(`Connecting to ${wsHost}`);
|
|
2167
|
-
logger.info("Connecting to remote management", { wsUrl });
|
|
2168
|
-
};
|
|
2169
|
-
while (!_stopRequested) {
|
|
2170
|
-
logConnecting();
|
|
2171
|
-
setRemoteManagementState({ status: RemoteManagementStatus.Connecting, errorMessage: "" });
|
|
2172
|
-
try {
|
|
2173
|
-
await handleWebSocketConnection(wsUrl, wsHost, token);
|
|
2174
|
-
} catch (error) {
|
|
2175
|
-
logger.warn("Remote management connection error", { error: String(error) });
|
|
2176
|
-
}
|
|
2177
|
-
if (_stopRequested) break;
|
|
2178
|
-
printer_default.warn(`Remote management disconnected. Reconnecting in ${RECONNECT_SLEEP_MS / 1e3} seconds...`);
|
|
2179
|
-
logger.info("Reconnecting to remote management after disconnect");
|
|
2180
|
-
await sleep(RECONNECT_SLEEP_MS);
|
|
2181
|
-
}
|
|
2182
|
-
process.removeListener("SIGINT", sigintHandler);
|
|
2183
|
-
logger.info("Remote management stopped.");
|
|
2184
|
-
return getRemoteManagementState();
|
|
2185
|
-
}
|
|
2186
|
-
async function handleWebSocketConnection(wsUrl, wsHost, token) {
|
|
2187
|
-
return new Promise((resolve) => {
|
|
2188
|
-
const ws = new WebSocket(wsUrl, {
|
|
2189
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
2190
|
-
});
|
|
2191
|
-
currentWs = ws;
|
|
2192
|
-
let heartbeat = null;
|
|
2193
|
-
let firstMessage = true;
|
|
2194
|
-
const cleanup = () => {
|
|
2195
|
-
if (heartbeat) clearInterval(heartbeat);
|
|
2196
|
-
currentWs = null;
|
|
2197
|
-
resolve();
|
|
2198
|
-
};
|
|
2199
|
-
ws.once("open", () => {
|
|
2200
|
-
printer_default.success(`Connected to ${wsHost}`);
|
|
2201
|
-
heartbeat = setInterval(() => {
|
|
2202
|
-
if (ws.readyState === WebSocket.OPEN) ws.ping();
|
|
2203
|
-
}, PING_INTERVAL_MS);
|
|
2204
|
-
});
|
|
2205
|
-
ws.on("ping", () => ws.pong());
|
|
2206
|
-
ws.on("message", async (data) => {
|
|
2207
|
-
try {
|
|
2208
|
-
if (firstMessage) {
|
|
2209
|
-
firstMessage = false;
|
|
2210
|
-
const ok = handleConnectionStatusMessage(data);
|
|
2211
|
-
if (!ok) ws.close();
|
|
2212
|
-
return;
|
|
2213
|
-
}
|
|
2214
|
-
setRemoteManagementState({ status: RemoteManagementStatus.Running, errorMessage: "" });
|
|
2215
|
-
const req = JSON.parse(data.toString("utf8"));
|
|
2216
|
-
await new WebSocketCommandHandler().handle(ws, req);
|
|
2217
|
-
} catch (e) {
|
|
2218
|
-
logger.warn("Failed handling websocket message", { error: String(e) });
|
|
2219
|
-
}
|
|
2220
|
-
});
|
|
2221
|
-
ws.on("unexpected-response", (_, res) => {
|
|
2222
|
-
setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: `HTTP ${res.statusCode}` });
|
|
2223
|
-
if (res.statusCode === 401) {
|
|
2224
|
-
printer_default.error("Unauthorized. Please enter a valid token.");
|
|
2225
|
-
logger.error("Unauthorized (401) on remote management connect");
|
|
2226
|
-
} else {
|
|
2227
|
-
printer_default.warn(`Unexpected HTTP ${res.statusCode}. Retrying...`);
|
|
2228
|
-
logger.warn("Unexpected HTTP response", { statusCode: res.statusCode });
|
|
2229
|
-
}
|
|
2230
|
-
ws.close();
|
|
2231
|
-
});
|
|
2232
|
-
ws.on("close", (code, reason) => {
|
|
2233
|
-
setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: "" });
|
|
2234
|
-
logger.info("WebSocket closed", { code, reason: reason.toString() });
|
|
2235
|
-
printer_default.warn(`Disconnected (code: ${code}). Retrying...`);
|
|
2236
|
-
cleanup();
|
|
2237
|
-
});
|
|
2238
|
-
ws.on("error", (err) => {
|
|
2239
|
-
setRemoteManagementState({ status: RemoteManagementStatus.Error, errorMessage: err.message });
|
|
2240
|
-
logger.warn("WebSocket error", { error: err.message });
|
|
2241
|
-
printer_default.error(err);
|
|
2242
|
-
cleanup();
|
|
2243
|
-
});
|
|
2244
|
-
});
|
|
2245
|
-
}
|
|
2246
|
-
async function closeRemoteManagement(timeoutMs = 1e4) {
|
|
2247
|
-
_stopRequested = true;
|
|
2248
|
-
try {
|
|
2249
|
-
if (currentWs) {
|
|
2250
|
-
try {
|
|
2251
|
-
setRemoteManagementState({ status: RemoteManagementStatus.Disconnecting, errorMessage: "" });
|
|
2252
|
-
currentWs.close();
|
|
2253
|
-
} catch (e) {
|
|
2254
|
-
logger.warn("Error while closing current remote management websocket", { error: String(e) });
|
|
2255
|
-
}
|
|
2256
|
-
}
|
|
2257
|
-
const start = Date.now();
|
|
2258
|
-
while (_remoteManagementState.status === "RUNNING") {
|
|
2259
|
-
if (Date.now() - start > timeoutMs) {
|
|
2260
|
-
logger.warn("Timed out waiting for remote management to stop");
|
|
2261
|
-
break;
|
|
2262
|
-
}
|
|
2263
|
-
await sleep(200);
|
|
2264
|
-
}
|
|
2265
|
-
} finally {
|
|
2266
|
-
currentWs = null;
|
|
2267
|
-
setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: "" });
|
|
2268
|
-
return getRemoteManagementState();
|
|
2269
|
-
}
|
|
2270
|
-
}
|
|
2271
|
-
function getRemoteManagementState() {
|
|
2272
|
-
return _remoteManagementState;
|
|
2273
|
-
}
|
|
2274
|
-
function setRemoteManagementState(state, errorMessage) {
|
|
2275
|
-
_remoteManagementState = {
|
|
2276
|
-
status: state.status,
|
|
2277
|
-
errorMessage: errorMessage || ""
|
|
2278
|
-
};
|
|
2279
|
-
}
|
|
2280
|
-
|
|
2281
649
|
// src/utils/parseArgs.ts
|
|
2282
650
|
import { parseArgs as parseArgs2 } from "util";
|
|
2283
651
|
import * as os from "os";
|
|
@@ -2376,7 +744,7 @@ async function createQrCodes(urls) {
|
|
|
2376
744
|
}
|
|
2377
745
|
|
|
2378
746
|
// src/tui/blessed/webDebuggerConnection.ts
|
|
2379
|
-
import
|
|
747
|
+
import WebSocket from "ws";
|
|
2380
748
|
|
|
2381
749
|
// src/tui/blessed/config.ts
|
|
2382
750
|
var defaultTuiConfig = {
|
|
@@ -2419,7 +787,7 @@ function createWebDebuggerConnection(webDebuggerUrl, onUpdate) {
|
|
|
2419
787
|
trimPairs();
|
|
2420
788
|
};
|
|
2421
789
|
const connect = () => {
|
|
2422
|
-
const ws = new
|
|
790
|
+
const ws = new WebSocket(`ws://${webDebuggerUrl}/introspec/websocket`);
|
|
2423
791
|
socket = ws;
|
|
2424
792
|
ws.on("open", () => {
|
|
2425
793
|
logger.info("Web debugger connected.");
|
|
@@ -3022,6 +1390,91 @@ function closeDisconnectModal(screen, manager) {
|
|
|
3022
1390
|
manager.inDisconnectView = false;
|
|
3023
1391
|
screen.render();
|
|
3024
1392
|
}
|
|
1393
|
+
function showReconnectingModal(screen, manager, retryCnt, message) {
|
|
1394
|
+
if (manager.reconnectModal) {
|
|
1395
|
+
manager.reconnectModal.destroy();
|
|
1396
|
+
manager.reconnectModal = null;
|
|
1397
|
+
}
|
|
1398
|
+
manager.inReconnectView = true;
|
|
1399
|
+
manager.reconnectModal = blessed2.box({
|
|
1400
|
+
parent: screen,
|
|
1401
|
+
top: "center",
|
|
1402
|
+
left: "center",
|
|
1403
|
+
width: "50%",
|
|
1404
|
+
height: "20%",
|
|
1405
|
+
border: {
|
|
1406
|
+
type: "line"
|
|
1407
|
+
},
|
|
1408
|
+
style: {
|
|
1409
|
+
border: {
|
|
1410
|
+
fg: "yellow"
|
|
1411
|
+
}
|
|
1412
|
+
},
|
|
1413
|
+
padding: { left: 2, right: 2, top: 1, bottom: 1 },
|
|
1414
|
+
tags: true,
|
|
1415
|
+
align: "center",
|
|
1416
|
+
valign: "middle"
|
|
1417
|
+
});
|
|
1418
|
+
const content = `{yellow-fg}{bold}Reconnecting...{/bold}{/yellow-fg}
|
|
1419
|
+
|
|
1420
|
+
${message || `Attempt #${retryCnt} \u2014 trying to re-establish tunnel...`}
|
|
1421
|
+
|
|
1422
|
+
{gray-fg}Please wait{/gray-fg}`;
|
|
1423
|
+
manager.reconnectModal.setContent(content);
|
|
1424
|
+
manager.reconnectModal.focus();
|
|
1425
|
+
screen.render();
|
|
1426
|
+
}
|
|
1427
|
+
function closeReconnectingModal(screen, manager) {
|
|
1428
|
+
if (manager.reconnectModal) {
|
|
1429
|
+
manager.reconnectModal.destroy();
|
|
1430
|
+
manager.reconnectModal = null;
|
|
1431
|
+
}
|
|
1432
|
+
manager.inReconnectView = false;
|
|
1433
|
+
screen.render();
|
|
1434
|
+
}
|
|
1435
|
+
function showReconnectionFailedModal(screen, manager, retryCnt, onClose) {
|
|
1436
|
+
closeReconnectingModal(screen, manager);
|
|
1437
|
+
manager.inReconnectView = true;
|
|
1438
|
+
manager.reconnectModal = blessed2.box({
|
|
1439
|
+
parent: screen,
|
|
1440
|
+
top: "center",
|
|
1441
|
+
left: "center",
|
|
1442
|
+
width: "50%",
|
|
1443
|
+
height: "20%",
|
|
1444
|
+
border: {
|
|
1445
|
+
type: "line"
|
|
1446
|
+
},
|
|
1447
|
+
style: {
|
|
1448
|
+
border: {
|
|
1449
|
+
fg: "red"
|
|
1450
|
+
}
|
|
1451
|
+
},
|
|
1452
|
+
padding: { left: 2, right: 2, top: 1, bottom: 1 },
|
|
1453
|
+
tags: true,
|
|
1454
|
+
align: "center",
|
|
1455
|
+
valign: "middle"
|
|
1456
|
+
});
|
|
1457
|
+
const content = `{red-fg}{bold}Reconnection Failed{/bold}{/red-fg}
|
|
1458
|
+
|
|
1459
|
+
Failed to reconnect after ${retryCnt} attempts.
|
|
1460
|
+
Tunnel will be closed.
|
|
1461
|
+
|
|
1462
|
+
{white-bg}{black-fg}Closing in 5 seconds...{/black-fg}{/white-bg}`;
|
|
1463
|
+
manager.reconnectModal.setContent(content);
|
|
1464
|
+
manager.reconnectModal.focus();
|
|
1465
|
+
screen.render();
|
|
1466
|
+
const timeout = setTimeout(() => {
|
|
1467
|
+
closeReconnectingModal(screen, manager);
|
|
1468
|
+
if (onClose) onClose();
|
|
1469
|
+
}, 5e3);
|
|
1470
|
+
const keyHandler = () => {
|
|
1471
|
+
clearTimeout(timeout);
|
|
1472
|
+
closeReconnectingModal(screen, manager);
|
|
1473
|
+
if (onClose) onClose();
|
|
1474
|
+
};
|
|
1475
|
+
manager.reconnectModal.key(["escape", "enter", "space"], keyHandler);
|
|
1476
|
+
screen.key(["escape", "enter", "space"], keyHandler);
|
|
1477
|
+
}
|
|
3025
1478
|
function showLoadingModal(screen, modalManager, message = "Loading...") {
|
|
3026
1479
|
if (modalManager.loadingView) return;
|
|
3027
1480
|
modalManager.loadingBox = blessed2.box({
|
|
@@ -3291,9 +1744,11 @@ var TunnelTui = class {
|
|
|
3291
1744
|
detailModal: null,
|
|
3292
1745
|
keyBindingsModal: null,
|
|
3293
1746
|
disconnectModal: null,
|
|
1747
|
+
reconnectModal: null,
|
|
3294
1748
|
inDetailView: false,
|
|
3295
1749
|
keyBindingView: false,
|
|
3296
1750
|
inDisconnectView: false,
|
|
1751
|
+
inReconnectView: false,
|
|
3297
1752
|
loadingBox: null,
|
|
3298
1753
|
loadingView: false,
|
|
3299
1754
|
fetchAbortController: null
|
|
@@ -3321,8 +1776,8 @@ var TunnelTui = class {
|
|
|
3321
1776
|
this.setupKeyBindings();
|
|
3322
1777
|
}
|
|
3323
1778
|
setupStatsListener() {
|
|
3324
|
-
globalThis.__PINGGY_TUNNEL_STATS__ = (
|
|
3325
|
-
this.stats = { ...
|
|
1779
|
+
globalThis.__PINGGY_TUNNEL_STATS__ = (newStats) => {
|
|
1780
|
+
this.stats = { ...newStats };
|
|
3326
1781
|
this.updateStatsDisplay();
|
|
3327
1782
|
};
|
|
3328
1783
|
}
|
|
@@ -3494,6 +1949,25 @@ Tunnel will be closed.` : info.messages?.join("\n") || "Disconnect request recei
|
|
|
3494
1949
|
);
|
|
3495
1950
|
}
|
|
3496
1951
|
}
|
|
1952
|
+
updateReconnectingInfo(retryCnt, message) {
|
|
1953
|
+
showReconnectingModal(
|
|
1954
|
+
this.screen,
|
|
1955
|
+
this.modalManager,
|
|
1956
|
+
retryCnt,
|
|
1957
|
+
message
|
|
1958
|
+
);
|
|
1959
|
+
}
|
|
1960
|
+
closeReconnectingInfo() {
|
|
1961
|
+
closeReconnectingModal(this.screen, this.modalManager);
|
|
1962
|
+
}
|
|
1963
|
+
updateReconnectionFailed(retryCnt) {
|
|
1964
|
+
showReconnectionFailedModal(
|
|
1965
|
+
this.screen,
|
|
1966
|
+
this.modalManager,
|
|
1967
|
+
retryCnt,
|
|
1968
|
+
() => this.destroy()
|
|
1969
|
+
);
|
|
1970
|
+
}
|
|
3497
1971
|
start() {
|
|
3498
1972
|
this.screen.render();
|
|
3499
1973
|
}
|
|
@@ -3590,6 +2064,35 @@ async function startCli(finalConfig, manager) {
|
|
|
3590
2064
|
}
|
|
3591
2065
|
printer_default.print(pico.gray("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
3592
2066
|
printer_default.print(pico.gray("\nPress Ctrl+C to stop the tunnel.\n"));
|
|
2067
|
+
manager2.registerWillReconnectListener(tunnel.tunnelid, (tunnelId, error, messages) => {
|
|
2068
|
+
if (activeTui) {
|
|
2069
|
+
const msg = messages?.join("\n") || error || "Tunnel disconnected, reconnecting...";
|
|
2070
|
+
activeTui.updateReconnectingInfo(0, msg);
|
|
2071
|
+
} else if (finalConfig.autoReconnect) {
|
|
2072
|
+
printer_default.warn(error || "Tunnel connection reset");
|
|
2073
|
+
printer_default.startSpinner(messages?.join("\n"));
|
|
2074
|
+
}
|
|
2075
|
+
});
|
|
2076
|
+
manager2.registerReconnectingListener(tunnel.tunnelid, (tunnelId, retryCnt) => {
|
|
2077
|
+
if (activeTui) {
|
|
2078
|
+
activeTui.updateReconnectingInfo(retryCnt);
|
|
2079
|
+
} else if (finalConfig.autoReconnect) {
|
|
2080
|
+
printer_default.startSpinner(`Reconnecting to Pinggy (attempt #${retryCnt})`);
|
|
2081
|
+
}
|
|
2082
|
+
});
|
|
2083
|
+
manager2.registerReconnectionCompletedListener(tunnel.tunnelid, (tunnelId, urls) => {
|
|
2084
|
+
if (activeTui) {
|
|
2085
|
+
activeTui.closeReconnectingInfo();
|
|
2086
|
+
}
|
|
2087
|
+
});
|
|
2088
|
+
manager2.registerReconnectionFailedListener(tunnel.tunnelid, (tunnelId, retryCnt) => {
|
|
2089
|
+
if (activeTui) {
|
|
2090
|
+
activeTui.updateReconnectionFailed(retryCnt);
|
|
2091
|
+
} else {
|
|
2092
|
+
printer_default.stopSpinnerFail(`Reconnection failed after ${retryCnt} attempts`);
|
|
2093
|
+
process.exit(1);
|
|
2094
|
+
}
|
|
2095
|
+
});
|
|
3593
2096
|
manager2.registerDisconnectListener(tunnel.tunnelid, async (tunnelId, error, messages) => {
|
|
3594
2097
|
if (activeTui) {
|
|
3595
2098
|
disconnectState = {
|
|
@@ -3668,7 +2171,7 @@ async function startCli(finalConfig, manager) {
|
|
|
3668
2171
|
}
|
|
3669
2172
|
|
|
3670
2173
|
// src/main.ts
|
|
3671
|
-
import { fileURLToPath
|
|
2174
|
+
import { fileURLToPath } from "url";
|
|
3672
2175
|
import { argv } from "process";
|
|
3673
2176
|
import { realpathSync } from "fs";
|
|
3674
2177
|
async function main() {
|
|
@@ -3706,7 +2209,7 @@ async function main() {
|
|
|
3706
2209
|
printer_default.error(error);
|
|
3707
2210
|
}
|
|
3708
2211
|
}
|
|
3709
|
-
var currentFile =
|
|
2212
|
+
var currentFile = fileURLToPath(import.meta.url);
|
|
3710
2213
|
var entryFile = null;
|
|
3711
2214
|
try {
|
|
3712
2215
|
entryFile = argv[1] ? realpathSync(argv[1]) : null;
|