pinggy 0.3.3 → 0.3.4

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/dist/index.js CHANGED
@@ -1,3853 +1,140 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- configureLogger,
4
- enablePackageLogging,
5
- logger
6
- } from "./chunk-HUN2MRZO.js";
3
+ printer_default
4
+ } from "./chunk-T5ESYDJY.js";
7
5
 
8
- // src/tunnel_manager/TunnelManager.ts
9
- import { pinggy } from "@pinggy/pinggy";
6
+ // src/utils/detect_vc_redist_on_windows.ts
7
+ import fs from "fs";
10
8
  import path from "path";
11
- import { Worker } from "worker_threads";
12
- import { fileURLToPath } from "url";
13
-
14
- // src/utils/printer.ts
15
- import pico2 from "picocolors";
16
-
17
- // src/tui/spinner/spinner.ts
18
- import pico from "picocolors";
19
- var spinners = {
20
- dots: {
21
- interval: 80,
22
- frames: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"]
23
- }
24
- };
25
- var currentTimer = null;
26
- var currentText = "";
27
- function startSpinner(name = "dots", text = "Loading") {
28
- const spinner = spinners[name];
29
- let i = 0;
30
- currentText = text;
31
- if (currentTimer) {
32
- clearInterval(currentTimer);
33
- }
34
- currentTimer = setInterval(() => {
35
- const frame = spinner.frames[i = ++i % spinner.frames.length];
36
- process.stdout.write(`\r${pico.cyan(frame)} ${text}`);
37
- }, spinner.interval);
38
- return () => stopSpinner();
39
- }
40
- function stopSpinner() {
41
- if (currentTimer) {
42
- clearInterval(currentTimer);
43
- currentTimer = null;
44
- process.stdout.write("\r\x1B[K");
45
- }
46
- }
47
- function stopSpinnerSuccess(message) {
48
- if (currentTimer) {
49
- clearInterval(currentTimer);
50
- currentTimer = null;
51
- const finalMessage = message || currentText;
52
- process.stdout.write(`\r${pico.green("\u2714")} ${finalMessage}
53
- `);
54
- }
55
- }
56
- function stopSpinnerFail(message) {
57
- if (currentTimer) {
58
- clearInterval(currentTimer);
59
- currentTimer = null;
60
- const finalMessage = message || currentText;
61
- process.stdout.write(`\r${pico.red("\u2716")} ${finalMessage}
62
- `);
63
- }
64
- }
65
-
66
- // src/utils/printer.ts
67
- var _CLIPrinter = class _CLIPrinter {
68
- static isCLIError(err) {
69
- return err instanceof Error;
70
- }
71
- static print(message, ...args) {
72
- console.log(message, ...args);
73
- }
74
- static error(err) {
75
- const def = this.errorDefinitions.find((d) => d.match(err));
76
- const msg = def.message(err);
77
- console.error(pico2.red(pico2.bold("\u2716 Error:")), pico2.red(msg));
78
- process.exit(1);
79
- }
80
- static warn(message) {
81
- console.warn(pico2.yellow(pico2.bold("\u26A0 Warning:")), pico2.yellow(message));
82
- }
83
- static warnTxt(message) {
84
- console.warn(pico2.yellow(pico2.bold("\u26A0 Warning:")), pico2.yellow(message));
85
- }
86
- static success(message) {
87
- console.log(pico2.green(pico2.bold(" \u2714 Success:")), pico2.green(message));
88
- }
89
- static async info(message) {
90
- console.log(pico2.blue(message));
91
- }
92
- static startSpinner(message) {
93
- startSpinner("dots", message);
94
- }
95
- static stopSpinnerSuccess(message) {
96
- stopSpinnerSuccess(message);
97
- }
98
- static stopSpinnerFail(message) {
99
- stopSpinnerFail(message);
100
- }
101
- };
102
- _CLIPrinter.errorDefinitions = [
103
- {
104
- match: (err) => _CLIPrinter.isCLIError(err) && err.code === "ERR_PARSE_ARGS_UNKNOWN_OPTION",
105
- message: (err) => {
106
- const match = /Unknown option '(.+?)'/.exec(err.message);
107
- const option = match ? match[1] : "(unknown)";
108
- return `Unknown option '${option}'. Please check your command or use pinggy --h for guidance.`;
109
- }
110
- },
111
- {
112
- match: (err) => _CLIPrinter.isCLIError(err) && err.code === "ERR_PARSE_ARGS_MISSING_OPTION_VALUE",
113
- message: (err) => `Missing required argument for option '${err.option}'.`
114
- },
115
- {
116
- match: (err) => _CLIPrinter.isCLIError(err) && err.code === "ERR_PARSE_ARGS_INVALID_OPTION_VALUE",
117
- message: (err) => `Invalid argument'${err.message}'.`
118
- },
119
- {
120
- match: (err) => _CLIPrinter.isCLIError(err) && err.code === "ENOENT",
121
- message: (err) => `File or directory not found: ${err.message}`
122
- },
123
- {
124
- match: () => true,
125
- // fallback
126
- message: (err) => _CLIPrinter.isCLIError(err) ? err.message : String(err)
127
- }
9
+ import { exec, execSync } from "child_process";
10
+ import os from "os";
11
+ import { promisify } from "util";
12
+ var execAsync = promisify(exec);
13
+ var DLLS = ["vcruntime140.dll", "vcruntime140_1.dll", "msvcp140.dll"];
14
+ var PATHS = ["C:\\Windows\\System32", "C:\\Windows\\SysWOW64"];
15
+ var REGISTRY_KEYS = [
16
+ "HKLM\\SOFTWARE\\Microsoft\\VisualStudio\\14.0\\VC\\Runtimes\\X64",
17
+ "HKLM\\SOFTWARE\\Microsoft\\VisualStudio\\14.0\\VC\\Runtimes\\X86",
18
+ "HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\VisualStudio\\14.0\\VC\\Runtimes\\X64",
19
+ "HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\VisualStudio\\14.0\\VC\\Runtimes\\X86"
128
20
  ];
129
- var CLIPrinter = _CLIPrinter;
130
- var printer_default = CLIPrinter;
131
-
132
- // src/utils/util.ts
133
- import { createRequire } from "module";
134
- import { randomUUID } from "crypto";
135
- function getRandomId() {
136
- return randomUUID();
137
- }
138
- function isValidPort(p) {
139
- return Number.isInteger(p) && p > 0 && p < 65536;
140
- }
141
- var require2 = createRequire(import.meta.url);
142
- var pkg = require2("../package.json");
143
- function getVersion() {
144
- return pkg.version ?? "";
145
- }
146
-
147
- // src/tunnel_manager/TunnelManager.ts
148
- var __filename2 = fileURLToPath(import.meta.url);
149
- var __dirname2 = path.dirname(__filename2);
150
- var TunnelManager = class _TunnelManager {
151
- constructor() {
152
- this.tunnelsByTunnelId = /* @__PURE__ */ new Map();
153
- this.tunnelsByConfigId = /* @__PURE__ */ new Map();
154
- this.tunnelStats = /* @__PURE__ */ new Map();
155
- this.tunnelStatsListeners = /* @__PURE__ */ new Map();
156
- this.tunnelErrorListeners = /* @__PURE__ */ new Map();
157
- this.tunnelDisconnectListeners = /* @__PURE__ */ new Map();
158
- this.tunnelWorkerErrorListeners = /* @__PURE__ */ new Map();
159
- this.tunnelStartListeners = /* @__PURE__ */ new Map();
160
- }
161
- static getInstance() {
162
- if (!_TunnelManager.instance) {
163
- _TunnelManager.instance = new _TunnelManager();
164
- }
165
- return _TunnelManager.instance;
166
- }
167
- /**
168
- * Creates a new managed tunnel instance with the given configuration.
169
- * Builds the config with forwarding rules and creates the tunnel instance.
170
- *
171
- * @param config - The tunnel configuration options
172
- * @param config.configid - Unique identifier for the tunnel configuration
173
- * @param config.tunnelid - Optional custom tunnel identifier. If not provided, a random UUID will be generated
174
- * @param config.additionalForwarding - Optional array of additional forwarding configurations
175
- *
176
- * @throws {Error} When configId is invalid or empty
177
- * @throws {Error} When a tunnel with the given configId already exists
178
- *
179
- * @returns {ManagedTunnel} A new managed tunnel instance containing the tunnel details,
180
- * status information, and statistics
181
- */
182
- async createTunnel(config) {
183
- const { configid, additionalForwarding, tunnelName } = config;
184
- if (configid === void 0 || configid.trim().length === 0) {
185
- throw new Error(`Invalid configId: "${configid}"`);
186
- }
187
- if (this.tunnelsByConfigId.has(configid)) {
188
- throw new Error(`Tunnel with configId "${configid}" already exists`);
189
- }
190
- const tunnelid = config.tunnelid || getRandomId();
191
- const configWithForwarding = this.buildPinggyConfig(config, additionalForwarding);
192
- return this._createTunnelWithProcessedConfig({
193
- configid,
194
- tunnelid,
195
- tunnelName,
196
- originalConfig: config,
197
- configWithForwarding,
198
- additionalForwarding,
199
- serve: config.serve,
200
- autoReconnect: config.autoReconnect !== void 0 ? config.autoReconnect : false
201
- });
202
- }
203
- /**
204
- * Internal method to create a tunnel with an already-processed configuration.
205
- * This is used by createTunnel, restartTunnel, and updateConfig to avoid config processing.
206
- *
207
- * @param params - Configuration parameters with already-processed forwarding rules
208
- * @returns The created ManagedTunnel instance
209
- * @private
210
- */
211
- async _createTunnelWithProcessedConfig(params) {
212
- let instance;
213
- try {
214
- instance = await pinggy.createTunnel(params.configWithForwarding);
215
- } catch (e) {
216
- logger.error("Error creating tunnel instance:", e);
217
- throw e;
218
- }
219
- const now = (/* @__PURE__ */ new Date()).toISOString();
220
- const managed = {
221
- tunnelid: params.tunnelid,
222
- configid: params.configid,
223
- tunnelName: params.tunnelName,
224
- instance,
225
- tunnelConfig: params.originalConfig,
226
- configWithForwarding: params.configWithForwarding,
227
- additionalForwarding: params.additionalForwarding,
228
- serve: params.serve,
229
- warnings: [],
230
- isStopped: false,
231
- createdAt: now,
232
- startedAt: null,
233
- stoppedAt: null,
234
- autoReconnect: params.autoReconnect
235
- };
236
- instance.setTunnelEstablishedCallback(({}) => {
237
- managed.startedAt = (/* @__PURE__ */ new Date()).toISOString();
238
- });
239
- this.setupStatsCallback(params.tunnelid, managed);
240
- this.setupErrorCallback(params.tunnelid, managed);
241
- this.setupDisconnectCallback(params.tunnelid, managed);
242
- this.setUpTunnelWorkerErrorCallback(params.tunnelid, managed);
243
- this.tunnelsByTunnelId.set(params.tunnelid, managed);
244
- this.tunnelsByConfigId.set(params.configid, managed);
245
- logger.info("Tunnel created", { configid: params.configid, tunnelid: params.tunnelid });
246
- return managed;
247
- }
248
- /**
249
- * Builds the Pinggy configuration by merging the default forwarding rule
250
- * with additional forwarding rules from additionalForwarding array.
251
- *
252
- * @param config - The base Pinggy configuration
253
- * @param additionalForwarding - Optional array of additional forwarding rules
254
- * @returns Modified PinggyOptions
255
- */
256
- buildPinggyConfig(config, additionalForwarding) {
257
- const forwardingRules = [];
258
- if (config.forwarding) {
259
- forwardingRules.push({
260
- type: config.tunnelType && config.tunnelType[0] || "http",
261
- address: config.forwarding
262
- });
263
- }
264
- if (Array.isArray(additionalForwarding) && additionalForwarding.length > 0) {
265
- for (const rule of additionalForwarding) {
266
- if (rule && rule.localDomain && rule.localPort && rule.remoteDomain && isValidPort(rule.localPort)) {
267
- const forwardingRule = {
268
- type: rule.protocol,
269
- // In Future we can make this dynamic based on user input
270
- address: `${rule.localDomain}:${rule.localPort}`,
271
- listenAddress: rule.remotePort && isValidPort(rule.remotePort) ? `${rule.remoteDomain}:${rule.remotePort}` : rule.remoteDomain
272
- };
273
- forwardingRules.push(forwardingRule);
274
- }
275
- }
276
- }
277
- return {
278
- ...config,
279
- forwarding: forwardingRules.length > 0 ? forwardingRules : config.forwarding
280
- };
281
- }
282
- /**
283
- * Start a tunnel that was created but not yet started
284
- */
285
- async startTunnel(tunnelId) {
286
- const managed = this.tunnelsByTunnelId.get(tunnelId);
287
- if (!managed) throw new Error(`Tunnel with id "${tunnelId}" not found`);
288
- logger.info("Starting tunnel", { tunnelId });
289
- let urls;
290
- try {
291
- urls = await managed.instance.start();
292
- } catch (error) {
293
- logger.error("Failed to start tunnel", { tunnelId, error });
294
- throw error;
295
- }
296
- logger.info("Tunnel started", { tunnelId, urls });
297
- if (managed.serve) {
298
- this.startStaticFileServer(managed);
299
- }
300
- try {
301
- const startListeners = this.tunnelStartListeners.get(tunnelId);
302
- if (startListeners) {
303
- for (const [id, listener] of startListeners) {
304
- try {
305
- listener(tunnelId, urls);
306
- } catch (err) {
307
- logger.debug("Error in start-listener callback", { listenerId: id, tunnelId, err });
308
- }
309
- }
310
- }
311
- } catch (e) {
312
- logger.warn("Failed to notify start listeners", { tunnelId, e });
313
- }
314
- return urls;
315
- }
316
- /**
317
- * Stops a running tunnel and updates its status.
318
- *
319
- * @param tunnelId - The unique identifier of the tunnel to stop
320
- * @throws {Error} If the tunnel with the given tunnelId is not found
321
- * @remarks
322
- * - Clears the tunnel's remote URLs
323
- * - Updates the tunnel's state to Exited if stopped successfully
324
- * - Logs the stop operation with tunnelId and configId
325
- */
326
- stopTunnel(tunnelId) {
327
- const managed = this.tunnelsByTunnelId.get(tunnelId);
328
- if (!managed) throw new Error(`Tunnel "${tunnelId}" not found`);
329
- logger.info("Stopping tunnel", { tunnelId, configId: managed.configid });
330
- try {
331
- managed.instance.stop();
332
- if (managed.serveWorker) {
333
- logger.info("terminating serveWorker");
334
- managed.serveWorker.terminate();
335
- }
336
- this.tunnelStats.delete(tunnelId);
337
- this.tunnelStatsListeners.delete(tunnelId);
338
- this.tunnelStats.delete(tunnelId);
339
- this.tunnelStatsListeners.delete(tunnelId);
340
- this.tunnelErrorListeners.delete(tunnelId);
341
- this.tunnelDisconnectListeners.delete(tunnelId);
342
- this.tunnelWorkerErrorListeners.delete(tunnelId);
343
- this.tunnelStartListeners.delete(tunnelId);
344
- managed.serveWorker = null;
345
- managed.warnings = managed.warnings ?? [];
346
- managed.isStopped = true;
347
- managed.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
348
- logger.info("Tunnel stopped", { tunnelId, configId: managed.configid });
349
- return { configid: managed.configid, tunnelid: managed.tunnelid };
350
- } catch (error) {
351
- logger.error("Failed to stop tunnel", { tunnelId, error });
352
- throw error;
353
- }
354
- }
355
- /**
356
- * Get all public URLs for a tunnel
357
- */
358
- async getTunnelUrls(tunnelId) {
359
- try {
360
- const managed = this.tunnelsByTunnelId.get(tunnelId);
361
- if (!managed || managed.isStopped) {
362
- logger.error(`Tunnel "${tunnelId}" not found when fetching URLs`);
363
- return [];
364
- }
365
- const urls = await managed.instance.urls();
366
- logger.debug("Queried tunnel URLs", { tunnelId, urls });
367
- return urls;
368
- } catch (error) {
369
- logger.error("Error fetching tunnel URLs", { tunnelId, error });
370
- throw error;
371
- }
372
- }
373
- /**
374
- * Get all TunnelStatus currently managed by this TunnelManager
375
- * @returns An array of all TunnelStatus objects
376
- */
377
- async getAllTunnels() {
378
- try {
379
- const tunnelList = await Promise.all(Array.from(this.tunnelsByTunnelId.values()).map(async (tunnel) => {
380
- return {
381
- tunnelid: tunnel.tunnelid,
382
- configid: tunnel.configid,
383
- tunnelName: tunnel.tunnelName,
384
- tunnelConfig: tunnel.tunnelConfig,
385
- remoteurls: !tunnel.isStopped ? await this.getTunnelUrls(tunnel.tunnelid) : [],
386
- additionalForwarding: tunnel.additionalForwarding,
387
- serve: tunnel.serve
388
- };
389
- }));
390
- return tunnelList;
391
- } catch (err) {
392
- logger.error("Error fetching tunnels", { error: err });
393
- return [];
394
- }
395
- }
396
- /**
397
- * Get status of a tunnel
398
- */
399
- async getTunnelStatus(tunnelId) {
400
- const managed = this.tunnelsByTunnelId.get(tunnelId);
401
- if (!managed) {
402
- logger.error(`Tunnel "${tunnelId}" not found when fetching status`);
403
- throw new Error(`Tunnel "${tunnelId}" not found`);
404
- }
405
- if (managed.isStopped) {
406
- return "exited";
407
- }
408
- const status = await managed.instance.getStatus();
409
- logger.debug("Queried tunnel status", { tunnelId, status });
410
- return status;
411
- }
412
- /**
413
- * Stop all tunnels
414
- */
415
- stopAllTunnels() {
416
- for (const { instance } of this.tunnelsByTunnelId.values()) {
417
- try {
418
- instance.stop();
419
- } catch (e) {
420
- logger.warn("Error stopping tunnel instance", e);
421
- }
422
- }
423
- this.tunnelsByTunnelId.clear();
424
- this.tunnelsByConfigId.clear();
425
- this.tunnelStats.clear();
426
- this.tunnelStatsListeners.clear();
427
- logger.info("All tunnels stopped and cleared");
428
- }
429
- /**
430
- * Remove a stopped tunnel's records so it will no longer be returned by list methods.
431
- *
432
- *
433
- * @param tunnelId - the tunnel id to remove
434
- * @returns true if the record was removed, false otherwise
435
- */
436
- removeStoppedTunnelByTunnelId(tunnelId) {
437
- const managed = this.tunnelsByTunnelId.get(tunnelId);
438
- if (!managed) {
439
- logger.debug("Attempted to remove non-existent tunnel", { tunnelId });
440
- return false;
441
- }
442
- if (!managed.isStopped) {
443
- logger.warn("Attempted to remove tunnel that is not stopped", { tunnelId });
444
- return false;
445
- }
446
- this._cleanupTunnelRecords(managed);
447
- logger.info("Removed stopped tunnel records", { tunnelId, configId: managed.configid });
448
- return true;
449
- }
450
- /**
451
- * Remove a stopped tunnel by its config id.
452
- * @param configId - the config id to remove
453
- * @returns true if the record was removed, false otherwise
454
- */
455
- removeStoppedTunnelByConfigId(configId) {
456
- const managed = this.tunnelsByConfigId.get(configId);
457
- if (!managed) {
458
- logger.debug("Attempted to remove non-existent tunnel by configId", { configId });
459
- return false;
460
- }
461
- return this.removeStoppedTunnelByTunnelId(managed.tunnelid);
462
- }
463
- _cleanupTunnelRecords(managed) {
464
- if (!managed.isStopped) {
465
- throw new Error(`Active tunnel "${managed.tunnelid}" cannot be removed`);
466
- }
467
- try {
468
- if (managed.serveWorker) {
469
- managed.serveWorker = null;
470
- }
471
- this.tunnelStats.delete(managed.tunnelid);
472
- this.tunnelStatsListeners.delete(managed.tunnelid);
473
- this.tunnelErrorListeners.delete(managed.tunnelid);
474
- this.tunnelDisconnectListeners.delete(managed.tunnelid);
475
- this.tunnelWorkerErrorListeners.delete(managed.tunnelid);
476
- this.tunnelStartListeners.delete(managed.tunnelid);
477
- this.tunnelsByTunnelId.delete(managed.tunnelid);
478
- this.tunnelsByConfigId.delete(managed.configid);
479
- } catch (e) {
480
- logger.warn("Failed cleaning up tunnel records", { tunnelId: managed.tunnelid, error: e });
481
- }
482
- }
483
- /**
484
- * Get tunnel instance by either configId or tunnelId
485
- * @param configId - The configuration ID of the tunnel
486
- * @param tunnelId - The tunnel ID
487
- * @returns The tunnel instance
488
- * @throws Error if neither configId nor tunnelId is provided, or if tunnel is not found
489
- */
490
- getTunnelInstance(configId, tunnelId) {
491
- if (configId) {
492
- const managed = this.tunnelsByConfigId.get(configId);
493
- if (!managed) throw new Error(`Tunnel "${configId}" not found`);
494
- return managed.instance;
495
- }
496
- if (tunnelId) {
497
- const managed = this.tunnelsByTunnelId.get(tunnelId);
498
- if (!managed) throw new Error(`Tunnel "${tunnelId}" not found`);
499
- return managed.instance;
500
- }
501
- throw new Error(`Either configId or tunnelId must be provided`);
502
- }
503
- /**
504
- * Get tunnel config by either configId or tunnelId
505
- * @param configId - The configuration ID of the tunnel
506
- * @param tunnelId - The tunnel ID
507
- * @returns The tunnel config
508
- * @throws Error if neither configId nor tunnelId is provided, or if tunnel is not found
509
- */
510
- async getTunnelConfig(configId, tunnelId) {
511
- if (configId) {
512
- const managed = this.tunnelsByConfigId.get(configId);
513
- if (!managed) {
514
- throw new Error(`Tunnel with configId "${configId}" not found`);
515
- }
516
- return managed.instance.getConfig();
517
- }
518
- if (tunnelId) {
519
- const managed = this.tunnelsByTunnelId.get(tunnelId);
520
- if (!managed) {
521
- throw new Error(`Tunnel with tunnelId "${tunnelId}" not found`);
522
- }
523
- return managed.instance.getConfig();
524
- }
525
- throw new Error(`Either configId or tunnelId must be provided`);
526
- }
527
- /**
528
- * Restarts a tunnel with its current configuration.
529
- * This function will stop the tunnel if it's running and start it again.
530
- * All configurations including additional forwarding rules are preserved.
531
- */
532
- async restartTunnel(tunnelid) {
533
- const existingTunnel = this.tunnelsByTunnelId.get(tunnelid);
534
- if (!existingTunnel) {
535
- throw new Error(`Tunnel "${tunnelid}" not found`);
536
- }
537
- logger.info("Initiating tunnel restart", {
538
- tunnelId: tunnelid,
539
- configId: existingTunnel.configid
540
- });
541
- try {
542
- const tunnelName = existingTunnel.tunnelName;
543
- const currentConfigId = existingTunnel.configid;
544
- const currentConfig = existingTunnel.tunnelConfig;
545
- const configWithForwarding = existingTunnel.configWithForwarding;
546
- const additionalForwarding = existingTunnel.additionalForwarding;
547
- const currentServe = existingTunnel.serve;
548
- const autoReconnect = existingTunnel.autoReconnect || false;
549
- this.tunnelsByTunnelId.delete(tunnelid);
550
- this.tunnelsByConfigId.delete(existingTunnel.configid);
551
- this.tunnelStats.delete(tunnelid);
552
- this.tunnelStatsListeners.delete(tunnelid);
553
- this.tunnelErrorListeners.delete(tunnelid);
554
- this.tunnelDisconnectListeners.delete(tunnelid);
555
- this.tunnelWorkerErrorListeners.delete(tunnelid);
556
- this.tunnelStartListeners.delete(tunnelid);
557
- const newTunnel = await this._createTunnelWithProcessedConfig({
558
- configid: currentConfigId,
559
- tunnelid,
560
- tunnelName,
561
- originalConfig: currentConfig,
562
- configWithForwarding,
563
- additionalForwarding,
564
- serve: currentServe,
565
- autoReconnect
566
- });
567
- if (existingTunnel.createdAt) {
568
- newTunnel.createdAt = existingTunnel.createdAt;
569
- }
570
- await this.startTunnel(newTunnel.tunnelid);
571
- } catch (error) {
572
- logger.error("Failed to restart tunnel", {
573
- tunnelid,
574
- error: error instanceof Error ? error.message : "Unknown error"
575
- });
576
- throw new Error(`Failed to restart tunnel: ${error instanceof Error ? error.message : "Unknown error"}`);
577
- }
578
- }
579
- /**
580
- * Updates the configuration of an existing tunnel.
581
- *
582
- * This method handles the process of updating a tunnel's configuration while preserving
583
- * its state. If the tunnel is running, it will be stopped, updated, and restarted.
584
- * In case of failure, it attempts to restore the original configuration.
585
- *
586
- * @param newConfig - The new configuration to apply, including configid and optional additional forwarding
587
- *
588
- * @returns Promise resolving to the updated ManagedTunnel
589
- * @throws Error if the tunnel is not found or if the update process fails
590
- */
591
- async updateConfig(newConfig) {
592
- const { configid, tunnelName: newTunnelName, additionalForwarding } = newConfig;
593
- if (!configid || configid.trim().length === 0) {
594
- throw new Error(`Invalid configid: "${configid}"`);
595
- }
596
- const existingTunnel = this.tunnelsByConfigId.get(configid);
597
- if (!existingTunnel) {
598
- throw new Error(`Tunnel with config id "${configid}" not found`);
599
- }
600
- const isStopped = existingTunnel.isStopped;
601
- const currentTunnelConfig = existingTunnel.tunnelConfig;
602
- const currentConfigWithForwarding = existingTunnel.configWithForwarding;
603
- const currentTunnelId = existingTunnel.tunnelid;
604
- const currentTunnelConfigId = existingTunnel.configid;
605
- const currentAdditionalForwarding = existingTunnel.additionalForwarding;
606
- const currentTunnelName = existingTunnel.tunnelName;
607
- const currentServe = existingTunnel.serve;
608
- const currentAutoReconnect = existingTunnel.autoReconnect || false;
609
- try {
610
- if (!isStopped) {
611
- existingTunnel.instance.stop();
612
- }
613
- this.tunnelsByTunnelId.delete(currentTunnelId);
614
- this.tunnelsByConfigId.delete(currentTunnelConfigId);
615
- const mergedBaseConfig = {
616
- ...newConfig,
617
- configid,
618
- tunnelName: newTunnelName !== void 0 ? newTunnelName : currentTunnelName,
619
- serve: newConfig.serve !== void 0 ? newConfig.serve : currentServe
620
- };
621
- const newConfigWithForwarding = this.buildPinggyConfig(
622
- mergedBaseConfig,
623
- additionalForwarding !== void 0 ? additionalForwarding : currentAdditionalForwarding
624
- );
625
- const newTunnel = await this._createTunnelWithProcessedConfig({
626
- configid,
627
- tunnelid: currentTunnelId,
628
- tunnelName: newTunnelName !== void 0 ? newTunnelName : currentTunnelName,
629
- originalConfig: mergedBaseConfig,
630
- configWithForwarding: newConfigWithForwarding,
631
- additionalForwarding: additionalForwarding !== void 0 ? additionalForwarding : currentAdditionalForwarding,
632
- serve: newConfig.serve !== void 0 ? newConfig.serve : currentServe,
633
- autoReconnect: currentAutoReconnect
634
- });
635
- if (!isStopped) {
636
- await this.startTunnel(newTunnel.tunnelid);
637
- }
638
- logger.info("Tunnel configuration updated", {
639
- tunnelId: newTunnel.tunnelid,
640
- configId: newTunnel.configid,
641
- isStopped
642
- });
643
- return newTunnel;
644
- } catch (error) {
645
- logger.error("Error updating tunnel configuration", {
646
- configId: configid,
647
- error: error instanceof Error ? error.message : String(error)
648
- });
21
+ function checkDLLs() {
22
+ return DLLS.every(
23
+ (dll) => PATHS.some((p) => {
649
24
  try {
650
- const originalTunnel = await this._createTunnelWithProcessedConfig({
651
- configid: currentTunnelConfigId,
652
- tunnelid: currentTunnelId,
653
- tunnelName: currentTunnelName,
654
- originalConfig: currentTunnelConfig,
655
- configWithForwarding: currentConfigWithForwarding,
656
- additionalForwarding: currentAdditionalForwarding,
657
- serve: currentServe,
658
- autoReconnect: currentAutoReconnect
659
- });
660
- if (!isStopped) {
661
- await this.startTunnel(originalTunnel.tunnelid);
662
- }
663
- logger.warn("Restored original tunnel configuration after update failure", {
664
- currentTunnelId,
665
- error: error instanceof Error ? error.message : "Unknown error"
666
- });
667
- } catch (restoreError) {
668
- logger.error("Failed to restore original tunnel configuration", {
669
- currentTunnelId,
670
- error: restoreError instanceof Error ? restoreError.message : "Unknown error"
671
- });
672
- }
673
- throw error;
674
- }
675
- }
676
- /**
677
- * Retrieve the ManagedTunnel object by either configId or tunnelId.
678
- * Throws an error if neither id is provided or the tunnel is not found.
679
- */
680
- getManagedTunnel(configId, tunnelId) {
681
- if (configId) {
682
- const managed = this.tunnelsByConfigId.get(configId);
683
- if (!managed) throw new Error(`Tunnel "${configId}" not found`);
684
- return managed;
685
- }
686
- if (tunnelId) {
687
- const managed = this.tunnelsByTunnelId.get(tunnelId);
688
- if (!managed) throw new Error(`Tunnel "${tunnelId}" not found`);
689
- return managed;
690
- }
691
- throw new Error(`Either configId or tunnelId must be provided`);
692
- }
693
- async getTunnelGreetMessage(tunnelId) {
694
- const managed = this.tunnelsByTunnelId.get(tunnelId);
695
- if (!managed) {
696
- logger.error(`Tunnel "${tunnelId}" not found when fetching greet message`);
697
- return null;
698
- }
699
- try {
700
- if (managed.isStopped) {
701
- return null;
702
- }
703
- const messages = await managed.instance.getGreetMessage();
704
- if (Array.isArray(messages)) {
705
- return messages.join(" ");
706
- }
707
- return messages ?? null;
708
- } catch (e) {
709
- logger.error(
710
- `Error fetching greet message for tunnel "${tunnelId}": ${e instanceof Error ? e.message : String(e)}`
711
- );
712
- return null;
713
- }
714
- }
715
- getTunnelStats(tunnelId) {
716
- const managed = this.tunnelsByTunnelId.get(tunnelId);
717
- if (!managed) {
718
- return null;
719
- }
720
- const stats = this.tunnelStats.get(tunnelId);
721
- return stats || null;
722
- }
723
- getLatestTunnelStats(tunnelId) {
724
- const managed = this.tunnelsByTunnelId.get(tunnelId);
725
- if (!managed) {
726
- return null;
727
- }
728
- const stats = this.tunnelStats.get(tunnelId);
729
- if (stats && stats.length > 0) {
730
- return stats[stats.length - 1];
731
- }
732
- return null;
733
- }
734
- /**
735
- * Registers a listener function to receive tunnel statistics updates.
736
- * The listener will be called whenever any tunnel's stats are updated.
737
- *
738
- * @param tunnelId - The tunnel ID to listen to stats for
739
- * @param listener - Function that receives tunnelId and stats when updates occur
740
- * @returns A unique listener ID that can be used to deregister the listener and tunnelId
741
- *
742
- * @throws {Error} When the specified tunnelId does not exist
743
- */
744
- async registerStatsListener(tunnelId, listener) {
745
- const managed = this.tunnelsByTunnelId.get(tunnelId);
746
- if (!managed) {
747
- throw new Error(`Tunnel "${tunnelId}" not found`);
748
- }
749
- if (!this.tunnelStatsListeners.has(tunnelId)) {
750
- this.tunnelStatsListeners.set(tunnelId, /* @__PURE__ */ new Map());
751
- }
752
- const listenerId = getRandomId();
753
- const tunnelListeners = this.tunnelStatsListeners.get(tunnelId);
754
- tunnelListeners.set(listenerId, listener);
755
- logger.info("Stats listener registered for tunnel", { tunnelId, listenerId });
756
- return [listenerId, tunnelId];
757
- }
758
- async registerErrorListener(tunnelId, listener) {
759
- const managed = this.tunnelsByTunnelId.get(tunnelId);
760
- if (!managed) {
761
- throw new Error(`Tunnel "${tunnelId}" not found`);
762
- }
763
- if (!this.tunnelErrorListeners.has(tunnelId)) {
764
- this.tunnelErrorListeners.set(tunnelId, /* @__PURE__ */ new Map());
765
- }
766
- const listenerId = getRandomId();
767
- const tunnelErrorListeners = this.tunnelErrorListeners.get(tunnelId);
768
- tunnelErrorListeners.set(listenerId, listener);
769
- logger.info("Error listener registered for tunnel", { tunnelId, listenerId });
770
- return listenerId;
771
- }
772
- async registerDisconnectListener(tunnelId, listener) {
773
- const managed = this.tunnelsByTunnelId.get(tunnelId);
774
- if (!managed) {
775
- throw new Error(`Tunnel "${tunnelId}" not found`);
776
- }
777
- if (!this.tunnelDisconnectListeners.has(tunnelId)) {
778
- this.tunnelDisconnectListeners.set(tunnelId, /* @__PURE__ */ new Map());
779
- }
780
- const listenerId = getRandomId();
781
- const tunnelDisconnectListeners = this.tunnelDisconnectListeners.get(tunnelId);
782
- tunnelDisconnectListeners.set(listenerId, listener);
783
- logger.info("Disconnect listener registered for tunnel", { tunnelId, listenerId });
784
- return listenerId;
785
- }
786
- async registerWorkerErrorListner(tunnelId, listener) {
787
- const managed = this.tunnelsByTunnelId.get(tunnelId);
788
- if (!managed) {
789
- throw new Error(`Tunnel "${tunnelId}" not found`);
790
- }
791
- if (!this.tunnelWorkerErrorListeners.has(tunnelId)) {
792
- this.tunnelWorkerErrorListeners.set(tunnelId, /* @__PURE__ */ new Map());
793
- }
794
- const listenerId = getRandomId();
795
- const tunnelWorkerErrorListner = this.tunnelWorkerErrorListeners.get(tunnelId);
796
- tunnelWorkerErrorListner?.set(listenerId, listener);
797
- logger.info("TunnelWorker error listener registered for tunnel", { tunnelId, listenerId });
798
- }
799
- async registerStartListener(tunnelId, listener) {
800
- const managed = this.tunnelsByTunnelId.get(tunnelId);
801
- if (!managed) {
802
- throw new Error(`Tunnel "${tunnelId}" not found`);
803
- }
804
- if (!this.tunnelStartListeners.has(tunnelId)) {
805
- this.tunnelStartListeners.set(tunnelId, /* @__PURE__ */ new Map());
806
- }
807
- const listenerId = getRandomId();
808
- const listeners = this.tunnelStartListeners.get(tunnelId);
809
- listeners.set(listenerId, listener);
810
- logger.info("Start listener registered for tunnel", { tunnelId, listenerId });
811
- return listenerId;
812
- }
813
- /**
814
- * Removes a previously registered stats listener.
815
- *
816
- * @param tunnelId - The tunnel ID the listener was registered for
817
- * @param listenerId - The unique ID returned when the listener was registered
818
- */
819
- deregisterStatsListener(tunnelId, listenerId) {
820
- const tunnelListeners = this.tunnelStatsListeners.get(tunnelId);
821
- if (!tunnelListeners) {
822
- logger.warn("No listeners found for tunnel", { tunnelId });
823
- return;
824
- }
825
- const removed = tunnelListeners.delete(listenerId);
826
- if (removed) {
827
- logger.info("Stats listener deregistered", { tunnelId, listenerId });
828
- if (tunnelListeners.size === 0) {
829
- this.tunnelStatsListeners.delete(tunnelId);
830
- }
831
- } else {
832
- logger.warn("Attempted to deregister non-existent stats listener", { tunnelId, listenerId });
833
- }
834
- }
835
- deregisterErrorListener(tunnelId, listenerId) {
836
- const listeners = this.tunnelErrorListeners.get(tunnelId);
837
- if (!listeners) {
838
- logger.warn("No error listeners found for tunnel", { tunnelId });
839
- return;
840
- }
841
- const removed = listeners.delete(listenerId);
842
- if (removed) {
843
- logger.info("Error listener deregistered", { tunnelId, listenerId });
844
- if (listeners.size === 0) {
845
- this.tunnelErrorListeners.delete(tunnelId);
846
- }
847
- } else {
848
- logger.warn("Attempted to deregister non-existent error listener", { tunnelId, listenerId });
849
- }
850
- }
851
- deregisterDisconnectListener(tunnelId, listenerId) {
852
- const listeners = this.tunnelDisconnectListeners.get(tunnelId);
853
- if (!listeners) {
854
- logger.warn("No disconnect listeners found for tunnel", { tunnelId });
855
- return;
856
- }
857
- const removed = listeners.delete(listenerId);
858
- if (removed) {
859
- logger.info("Disconnect listener deregistered", { tunnelId, listenerId });
860
- if (listeners.size === 0) {
861
- this.tunnelDisconnectListeners.delete(tunnelId);
862
- }
863
- } else {
864
- logger.warn("Attempted to deregister non-existent disconnect listener", { tunnelId, listenerId });
865
- }
866
- }
867
- async getLocalserverTlsInfo(tunnelId) {
868
- const managed = this.tunnelsByTunnelId.get(tunnelId);
869
- if (!managed) {
870
- logger.error(`Tunnel "${tunnelId}" not found when fetching local server TLS info`);
871
- return false;
872
- }
873
- try {
874
- if (managed.isStopped) {
25
+ return fs.existsSync(path.join(p, dll));
26
+ } catch {
875
27
  return false;
876
28
  }
877
- const tlsInfo = await managed.instance.getLocalServerTls();
878
- if (tlsInfo) {
879
- return tlsInfo;
880
- }
881
- return false;
882
- } catch (e) {
883
- logger.error(`Error fetching TLS info for tunnel "${tunnelId}": ${e instanceof Error ? e.message : e}`);
884
- return false;
885
- }
886
- }
887
- /**
888
- * Sets up the stats callback for a tunnel during creation.
889
- * This callback will update stored stats and notify all registered listeners.
890
- */
891
- setupStatsCallback(tunnelId, managed) {
892
- try {
893
- const callback = (usage) => {
894
- this.updateStats(tunnelId, usage);
895
- };
896
- managed.instance.setUsageUpdateCallback(callback);
897
- logger.debug("Stats callback set up for tunnel", { tunnelId });
898
- } catch (error) {
899
- logger.warn("Failed to set up stats callback", { tunnelId, error });
900
- }
901
- }
902
- notifyErrorListeners(tunnelId, errorMsg, isFatal) {
903
- try {
904
- const listeners = this.tunnelErrorListeners.get(tunnelId);
905
- if (!listeners) return;
906
- for (const [id, listener] of listeners) {
907
- try {
908
- listener(tunnelId, errorMsg, isFatal);
909
- } catch (err) {
910
- logger.debug("Error in error-listener callback", { listenerId: id, tunnelId, err });
911
- }
912
- }
913
- } catch (err) {
914
- logger.debug("Failed to notify error listeners", { tunnelId, err });
915
- }
916
- }
917
- setupErrorCallback(tunnelId, managed) {
918
- try {
919
- const callback = ({ errorNo, error, recoverable }) => {
920
- try {
921
- const msg = typeof error === "string" ? error : String(error);
922
- const isFatal = true;
923
- logger.debug("Tunnel reported error", { tunnelId, errorNo, errorMsg: msg, recoverable });
924
- this.notifyErrorListeners(tunnelId, msg, isFatal);
925
- } catch (e) {
926
- logger.warn("Error handling tunnel error callback", { tunnelId, e });
927
- }
928
- };
929
- managed.instance.setTunnelErrorCallback(callback);
930
- logger.debug("Error callback set up for tunnel", { tunnelId });
931
- } catch (error) {
932
- logger.warn("Failed to set up error callback", { tunnelId, error });
933
- }
934
- }
935
- setupDisconnectCallback(tunnelId, managed) {
936
- try {
937
- const callback = ({ error, messages }) => {
938
- try {
939
- logger.debug("Tunnel disconnected", { tunnelId, error, messages });
940
- const managedTunnel = this.tunnelsByTunnelId.get(tunnelId);
941
- if (managedTunnel) {
942
- managedTunnel.isStopped = true;
943
- managedTunnel.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
944
- }
945
- if (managedTunnel && managedTunnel.autoReconnect) {
946
- logger.info("Auto-reconnecting tunnel", { tunnelId });
947
- setTimeout(async () => {
948
- try {
949
- await this.restartTunnel(tunnelId);
950
- logger.info("Tunnel auto-reconnected successfully", { tunnelId });
951
- } catch (e) {
952
- logger.error("Failed to auto-reconnect tunnel", { tunnelId, e });
953
- }
954
- }, 1e4);
955
- }
956
- const listeners = this.tunnelDisconnectListeners.get(tunnelId);
957
- if (!listeners) return;
958
- for (const [id, listener] of listeners) {
959
- try {
960
- listener(tunnelId, error, messages);
961
- } catch (err) {
962
- logger.debug("Error in disconnect-listener callback", { listenerId: id, tunnelId, err });
963
- }
964
- }
965
- } catch (e) {
966
- logger.warn("Error handling tunnel disconnect callback", { tunnelId, e });
967
- }
968
- };
969
- managed.instance.setTunnelDisconnectedCallback(callback);
970
- logger.debug("Disconnect callback set up for tunnel", { tunnelId });
971
- } catch (error) {
972
- logger.warn("Failed to set up disconnect callback", { tunnelId, error });
973
- }
974
- }
975
- setUpTunnelWorkerErrorCallback(tunnelId, managed) {
976
- try {
977
- const callback = (error) => {
978
- try {
979
- logger.debug("Error in Tunnel Worker", { tunnelId, errorMessage: error.message });
980
- const listeners = this.tunnelWorkerErrorListeners.get(tunnelId);
981
- if (!listeners) return;
982
- for (const [id, listener] of listeners) {
983
- try {
984
- listener(tunnelId, error);
985
- } catch (err) {
986
- logger.debug("Error in worker-error-listener callback", { listenerId: id, tunnelId, err });
987
- }
988
- }
989
- } catch (e) {
990
- logger.warn("Error handling tunnel worker error callback", { tunnelId, e });
991
- }
992
- };
993
- managed.instance.setWorkerErrorCallback(callback);
994
- logger.debug("Disconnect callback set up for tunnel", { tunnelId });
995
- } catch (error) {
996
- logger.warn("Failed to setup tunnel worker error callback");
997
- }
998
- }
999
- /**
1000
- * Updates the stored stats for a tunnel and notifies all registered listeners.
1001
- */
1002
- updateStats(tunnelId, rawUsage) {
1003
- try {
1004
- const normalizedStats = this.normalizeStats(rawUsage);
1005
- const existingStats = this.tunnelStats.get(tunnelId) || [];
1006
- const updatedStats = [...existingStats, normalizedStats];
1007
- this.tunnelStats.set(tunnelId, updatedStats);
1008
- const tunnelListeners = this.tunnelStatsListeners.get(tunnelId);
1009
- if (tunnelListeners) {
1010
- for (const [listenerId, listener] of tunnelListeners) {
1011
- try {
1012
- listener(tunnelId, normalizedStats);
1013
- } catch (error) {
1014
- logger.warn("Error in stats listener callback", { listenerId, tunnelId, error });
1015
- }
1016
- }
1017
- }
1018
- logger.debug("Stats updated and listeners notified", {
1019
- tunnelId,
1020
- listenersCount: tunnelListeners?.size || 0
1021
- });
1022
- } catch (error) {
1023
- logger.warn("Error updating stats", { tunnelId, error });
1024
- }
1025
- }
1026
- /**
1027
- * Normalizes raw usage data from the SDK into a consistent TunnelStats format.
1028
- */
1029
- normalizeStats(rawStats) {
1030
- const elapsed = this.parseNumber(rawStats.elapsedTime ?? 0);
1031
- const liveConns = this.parseNumber(rawStats.numLiveConnections ?? 0);
1032
- const totalConns = this.parseNumber(rawStats.numTotalConnections ?? 0);
1033
- const reqBytes = this.parseNumber(rawStats.numTotalReqBytes ?? 0);
1034
- const resBytes = this.parseNumber(rawStats.numTotalResBytes ?? 0);
1035
- const txBytes = this.parseNumber(rawStats.numTotalTxBytes ?? 0);
1036
- return {
1037
- elapsedTime: elapsed,
1038
- numLiveConnections: liveConns,
1039
- numTotalConnections: totalConns,
1040
- numTotalReqBytes: reqBytes,
1041
- numTotalResBytes: resBytes,
1042
- numTotalTxBytes: txBytes
1043
- };
1044
- }
1045
- parseNumber(value) {
1046
- const parsed = typeof value === "number" ? value : parseInt(String(value), 10);
1047
- return isNaN(parsed) ? 0 : parsed;
1048
- }
1049
- startStaticFileServer(managed) {
1050
- try {
1051
- const __filename3 = fileURLToPath(import.meta.url);
1052
- const __dirname3 = path.dirname(__filename3);
1053
- const fileServerWorkerPath = path.join(__dirname3, "workers", "file_serve_worker.cjs");
1054
- const staticServerWorker = new Worker(fileServerWorkerPath, {
1055
- workerData: {
1056
- dir: managed.serve,
1057
- port: managed.tunnelConfig?.forwarding
1058
- }
1059
- });
1060
- staticServerWorker.on("message", (msg) => {
1061
- switch (msg.type) {
1062
- case "started":
1063
- logger.info("Static file server started", { dir: managed.serve });
1064
- break;
1065
- case "warning":
1066
- if (msg.code === "INVALID_TUNNEL_SERVE_PATH") {
1067
- managed.warnings = managed.warnings ?? [];
1068
- managed.warnings.push({ code: msg.code, message: msg.message });
1069
- }
1070
- printer_default.warn(msg.message);
1071
- break;
1072
- case "error":
1073
- managed.warnings = managed.warnings ?? [];
1074
- managed.warnings.push({
1075
- code: "UNKNOWN_WARNING",
1076
- message: msg.message
1077
- });
1078
- break;
1079
- }
1080
- });
1081
- managed.serveWorker = staticServerWorker;
1082
- } catch (error) {
1083
- logger.error("Error starting static file server", error);
1084
- }
1085
- }
1086
- };
1087
-
1088
- // src/cli/options.ts
1089
- var cliOptions = {
1090
- // SSH-like options
1091
- R: { type: "string", multiple: true, description: "Local port. Eg. -R0:localhost:3000 will forward tunnel connections to local port 3000." },
1092
- L: { type: "string", multiple: true, description: "Web Debugger address. Eg. -L4300:localhost:4300 will start web debugger on port 4300." },
1093
- o: { type: "string", multiple: true, description: "Options", hidden: true },
1094
- "server-port": { type: "string", short: "p", description: "Pinggy server port. Default: 443" },
1095
- v4: { type: "boolean", short: "4", description: "IPv4 only", hidden: true },
1096
- v6: { type: "boolean", short: "6", description: "IPv6 only", hidden: true },
1097
- // These options appear in the ssh command, but we ignore it in CLI
1098
- t: { type: "boolean", description: "hidden", hidden: true },
1099
- T: { type: "boolean", description: "hidden", hidden: true },
1100
- n: { type: "boolean", description: "hidden", hidden: true },
1101
- N: { type: "boolean", description: "hidden", hidden: true },
1102
- // Better options
1103
- type: { type: "string", description: "Type of the connection. Eg. --type tcp" },
1104
- localport: { type: "string", short: "l", description: "Takes input as [protocol:][host:]port. Eg. --localport https://localhost:8000 OR -l 3000" },
1105
- debugger: { type: "string", short: "d", description: "Port for web debugger. Eg. --debugger 4300 OR -d 4300" },
1106
- token: { type: "string", description: "Token for authentication. Eg. --token TOKEN_VALUE" },
1107
- // Logging options (CLI overrides env)
1108
- loglevel: { type: "string", description: "Logging level: ERROR, INFO, DEBUG. Overrides PINGGY_LOG_LEVEL environment variable" },
1109
- logfile: { type: "string", description: "Path to log file. Overrides PINGGY_LOG_FILE environment variable" },
1110
- v: { type: "boolean", description: "Print logs to stdout for Cli. Overrides PINGGY_LOG_STDOUT environment variable" },
1111
- vv: { type: "boolean", description: "Enable detailed logging for the Node.js SDK and Libpinggy, including both info and debug level logs." },
1112
- vvv: { type: "boolean", description: "Enable all logs from Cli, SDK and internal components." },
1113
- autoreconnect: { type: "boolean", short: "a", description: "Automatically reconnect tunnel on failure." },
1114
- // Save and load config
1115
- saveconf: { type: "string", description: "Create the configuration file based on the options provided here" },
1116
- conf: { type: "string", description: "Use the configuration file as base. Other options will be used to override this file" },
1117
- // File server
1118
- serve: { type: "string", description: "Start a webserver to serve files from the specified path. Eg --serve /path/to/files" },
1119
- // Remote Control
1120
- "remote-management": { type: "string", description: "Enable remote management of tunnels with token. Eg. --remote-management API_KEY" },
1121
- manage: { type: "string", description: "Provide a server address to manage tunnels. Eg --manage dashboard.pinggy.io" },
1122
- notui: { type: "boolean", description: "Disable TUI in remote management mode" },
1123
- // Misc
1124
- version: { type: "boolean", description: "Print version" },
1125
- // Help
1126
- help: { type: "boolean", short: "h", description: "Show this help message" }
1127
- };
1128
-
1129
- // src/cli/help.ts
1130
- function printHelpMessage() {
1131
- console.log("\nPinggy CLI Tool - Create secure tunnels to your localhost.");
1132
- console.log("\nUsage:");
1133
- console.log(" pinggy [options] -l <port>\n");
1134
- console.log("Options:");
1135
- for (const [key, value] of Object.entries(cliOptions)) {
1136
- if (value.hidden) continue;
1137
- const short = "short" in value && value.short ? `-${value.short}, ` : " ";
1138
- const optType = value.type === "boolean" ? "" : "<value>";
1139
- console.log(` ${short}--${key.padEnd(17)} ${optType.padEnd(8)} ${value.description}`);
1140
- }
1141
- console.log("\nExtended options :");
1142
- console.log(" x:https Enforce HTTPS only (redirect HTTP to HTTPS)");
1143
- console.log(" x:noreverseproxy Disable built-in reverse-proxy header injection");
1144
- console.log(" x:localservertls:host Connect to local HTTPS server with SNI");
1145
- console.log(" x:passpreflight Pass CORS preflight requests unchanged");
1146
- console.log(" a:Key:Val Add header");
1147
- console.log(" u:Key:Val Update header");
1148
- console.log(" r:Key Remove header");
1149
- console.log(" b:user:pass Basic auth");
1150
- console.log(" k:BEARER Bearer token");
1151
- console.log(" w:192.168.1.0/24 IP whitelist (CIDR)");
1152
- console.log("\nExamples (User-friendly):");
1153
- console.log(" pinggy -l 3000 # HTTP(S) tunnel to localhost port 3000");
1154
- console.log(" pinggy --type tcp -l 22 # TCP tunnel for SSH (port 22)");
1155
- console.log(" pinggy -l 8080 -d 4300 # HTTP tunnel to port 8080 with debugger running at localhost:4300");
1156
- console.log(" pinggy --token mytoken -l 3000 # Authenticated tunnel");
1157
- console.log(" pinggy x:https x:xff -l https://localhost:8443 # HTTPS-only + XFF");
1158
- console.log(" pinggy w:192.168.1.0/24 -l 8080 # IP whitelist restriction");
1159
- console.log("\nExamples (SSH-style):");
1160
- console.log(" pinggy -R0:localhost:3000 # Basic HTTP tunnel");
1161
- console.log(" pinggy --type tcp -R0:localhost:22 # TCP tunnel for SSH");
1162
- console.log(" pinggy -R0:localhost:8080 -L4300:localhost:4300 # HTTP tunnel with debugger");
1163
- console.log(" pinggy tcp@ap.example.com -R0:localhost:22 # TCP tunnel to region\n");
1164
- }
1165
-
1166
- // src/cli/defaults.ts
1167
- var defaultOptions = {
1168
- token: void 0,
1169
- // No default token
1170
- serverAddress: "a.pinggy.io",
1171
- forwarding: "localhost:8000",
1172
- webDebugger: "",
1173
- ipWhitelist: [],
1174
- basicAuth: [],
1175
- bearerTokenAuth: [],
1176
- headerModification: [],
1177
- force: false,
1178
- xForwardedFor: false,
1179
- httpsOnly: false,
1180
- originalRequestUrl: false,
1181
- allowPreflight: false,
1182
- reverseProxy: false,
1183
- autoReconnect: false
1184
- };
1185
-
1186
- // src/cli/extendedOptions.ts
1187
- import { isIP } from "net";
1188
- function parseExtendedOptions(options, config) {
1189
- if (!options) return;
1190
- for (const opt of options) {
1191
- const [key, value] = opt.replace(/^"|"$/g, "").split(/:(.+)/).filter(Boolean);
1192
- switch (key) {
1193
- case "x":
1194
- switch (value) {
1195
- case "https":
1196
- case "httpsonly":
1197
- config.httpsOnly = true;
1198
- break;
1199
- case "passpreflight":
1200
- case "allowpreflight":
1201
- config.allowPreflight = true;
1202
- break;
1203
- case "reverseproxy":
1204
- config.reverseProxy = false;
1205
- break;
1206
- case "xff":
1207
- config.xForwardedFor = true;
1208
- break;
1209
- case "fullurl":
1210
- case "fullrequesturl":
1211
- config.originalRequestUrl = true;
1212
- break;
1213
- default:
1214
- printer_default.warn(`Unknown extended option "${key}"`);
1215
- logger.warn(`Warning: Unknown extended option "${key}"`);
1216
- break;
1217
- }
1218
- break;
1219
- case "w":
1220
- if (value) {
1221
- const ips = value.split(",").map((ip) => ip.trim()).filter(Boolean);
1222
- const invalidIps = ips.filter((ip) => !(isValidIpV4Cidr(ip) || isValidIpV6Cidr(ip)));
1223
- if (invalidIps.length > 0) {
1224
- printer_default.warn(`Invalid IP/CIDR(s) in whitelist: ${invalidIps.join(", ")}`);
1225
- logger.warn(`Warning: Invalid IP/CIDR(s) in whitelist: ${invalidIps.join(", ")}`);
1226
- }
1227
- if (!(invalidIps.length > 0)) {
1228
- config.ipWhitelist = ips;
1229
- }
1230
- } else {
1231
- printer_default.warn(`Extended option "${opt}" for 'w' requires IP(s)`);
1232
- logger.warn(`Warning: Extended option "${opt}" for 'w' requires IP(s)`);
1233
- }
1234
- break;
1235
- case "k":
1236
- if (!config.bearerTokenAuth) config.bearerTokenAuth = [];
1237
- if (value) {
1238
- config.bearerTokenAuth.push(value);
1239
- } else {
1240
- printer_default.warn(`Extended option "${opt}" for 'k' requires a value`);
1241
- logger.warn(`Warning: Extended option "${opt}" for 'k' requires a value`);
1242
- }
1243
- break;
1244
- case "b":
1245
- if (value && value.includes(":")) {
1246
- const [username, password] = value.split(/:(.+)/);
1247
- if (!config.basicAuth) config.basicAuth = [];
1248
- config.basicAuth.push({ username, password });
1249
- } else {
1250
- printer_default.warn(`Extended option "${opt}" for 'b' requires value in format username:password`);
1251
- logger.warn(`Warning: Extended option "${opt}" for 'b' requires value in format username:password`);
1252
- }
1253
- break;
1254
- case "a":
1255
- if (value && value.includes(":")) {
1256
- const [key2, val] = value.split(/:(.+)/);
1257
- if (!config.headerModification) config.headerModification = [];
1258
- config.headerModification.push({ type: "add", key: key2, value: [val] });
1259
- } else {
1260
- printer_default.warn(`Extended option "${opt}" for 'a' requires key:value`);
1261
- logger.warn(`Warning: Extended option "${opt}" for 'a' requires key:value`);
1262
- }
1263
- break;
1264
- case "u":
1265
- if (value && value.includes(":")) {
1266
- const [key2, val] = value.split(/:(.+)/);
1267
- if (!config.headerModification) config.headerModification = [];
1268
- config.headerModification.push({ type: "update", key: key2, value: [val] });
1269
- } else {
1270
- printer_default.warn(`Extended option "${opt}" for 'u' requires key:value`);
1271
- logger.warn(`Warning: Extended option "${opt}" for 'u' requires key:value`);
1272
- }
1273
- break;
1274
- case "r":
1275
- if (value) {
1276
- if (!config.headerModification) config.headerModification = [];
1277
- config.headerModification.push({ type: "remove", key: value });
1278
- } else {
1279
- printer_default.warn(`Extended option "${opt}" for 'r' requires a key`);
1280
- }
1281
- break;
1282
- default:
1283
- printer_default.warn(`Unknown extended option "${key}"`);
1284
- break;
1285
- }
1286
- }
29
+ })
30
+ );
1287
31
  }
1288
- function isValidIpV4Cidr(input) {
1289
- if (input.includes("/")) {
1290
- const [ip, mask] = input.split("/");
1291
- if (!ip || !mask) return false;
1292
- const isIp4 = isIP(ip) === 4;
1293
- const maskNum = parseInt(mask, 10);
1294
- const isMaskValid = !isNaN(maskNum) && maskNum >= 0 && maskNum <= 32;
1295
- return isIp4 && isMaskValid;
1296
- }
1297
- return false;
1298
- }
1299
- function isValidIpV6Cidr(input) {
1300
- if (input.includes("/")) {
1301
- const [rawIp, mask] = input.split("/");
1302
- if (!rawIp || !mask) return false;
1303
- const ip = rawIp.split("%")[0].replace(/^\[|\]$/g, "");
1304
- const isIp6 = isIP(ip) === 6;
1305
- const maskNum = parseInt(mask, 10);
1306
- const isMaskValid = !isNaN(maskNum) && maskNum >= 0 && maskNum <= 128;
1307
- return isIp6 && isMaskValid;
1308
- }
1309
- return false;
1310
- }
1311
-
1312
- // src/cli/buildConfig.ts
1313
- import { TunnelType as TunnelType2 } from "@pinggy/pinggy";
1314
- import fs from "fs";
1315
- import path2 from "path";
1316
- var domainRegex = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
1317
- function parseUserAndDomain(str) {
1318
- let token;
1319
- let type;
1320
- let server;
1321
- let qrCode;
1322
- if (!str) return { token, type, server, qrCode };
1323
- if (str.includes("@")) {
1324
- const [user, domain] = str.split("@", 2);
1325
- if (domainRegex.test(domain)) {
1326
- server = domain;
1327
- const parts = user.split("+");
1328
- for (const part of parts) {
1329
- if ([TunnelType2.Http, TunnelType2.Tcp, TunnelType2.Tls, TunnelType2.Udp, TunnelType2.TlsTcp].includes(part.toLowerCase())) {
1330
- type = part;
1331
- } else if (part === "force") {
1332
- token = (token ? token + "+" : "") + part;
1333
- } else if (part === "qr") {
1334
- qrCode = true;
1335
- } else {
1336
- token = (token ? token + "+" : "") + part;
1337
- }
32
+ function checkRegistry() {
33
+ if (os.platform() !== "win32") return false;
34
+ try {
35
+ for (const key of REGISTRY_KEYS) {
36
+ const cmd = `reg query "${key}" /v Installed 2>nul`;
37
+ const result = execSync(cmd, { encoding: "utf8" });
38
+ if (result.includes("0x1")) {
39
+ return true;
1338
40
  }
1339
41
  }
1340
- } else if (domainRegex.test(str)) {
1341
- server = str;
42
+ } catch {
1342
43
  }
1343
- return { token, type, server, qrCode };
44
+ return false;
1344
45
  }
1345
- function parseUsers(positionalArgs, explicitToken) {
1346
- let token;
1347
- let server;
1348
- let type;
1349
- let forceFlag = false;
1350
- let qrCode = false;
1351
- let remaining = [...positionalArgs];
1352
- if (typeof explicitToken === "string") {
1353
- const parsed = parseUserAndDomain(explicitToken);
1354
- if (parsed.server) server = parsed.server;
1355
- if (parsed.type) type = parsed.type;
1356
- if (parsed.token) token = parsed.token;
1357
- }
1358
- if (remaining.length > 0) {
1359
- const first = remaining[0];
1360
- const parsed = parseUserAndDomain(first);
1361
- if (parsed.server) {
1362
- server = parsed.server;
1363
- if (parsed.type) type = parsed.type;
1364
- if (parsed.token) {
1365
- if (parsed.token.includes("+")) {
1366
- const parts = parsed.token.split("+");
1367
- const tOnly = parts.filter((p) => p !== "force").join("+");
1368
- if (tOnly) token = tOnly;
1369
- if (parts.includes("force")) forceFlag = true;
1370
- } else {
1371
- token = parsed.token;
1372
- }
1373
- }
1374
- if (parsed.qrCode) {
1375
- qrCode = true;
46
+ function getVCRedistVersion() {
47
+ if (os.platform() !== "win32") return null;
48
+ try {
49
+ for (const key of REGISTRY_KEYS) {
50
+ const cmd = `reg query "${key}" /v Version 2>nul`;
51
+ const result = execSync(cmd, { encoding: "utf8" });
52
+ const match = result.match(/Version\s+REG_SZ\s+(\S+)/);
53
+ if (match) {
54
+ return match[1];
1376
55
  }
1377
- remaining = remaining.slice(1);
1378
- }
1379
- }
1380
- return { token, server, type, forceFlag, qrCode, remaining };
1381
- }
1382
- function parseType(finalConfig, values, inferredType) {
1383
- const t = inferredType || values.type || finalConfig.tunnelType;
1384
- if (t === TunnelType2.Http || t === TunnelType2.Tcp || t === TunnelType2.Tls || t === TunnelType2.Udp || t === TunnelType2.TlsTcp) {
1385
- finalConfig.tunnelType = [t];
1386
- }
1387
- }
1388
- function parseLocalPort(finalConfig, values) {
1389
- if (typeof values.localport !== "string") return null;
1390
- let lp = values.localport.trim();
1391
- let isHttps = false;
1392
- if (lp.startsWith("https://")) {
1393
- isHttps = true;
1394
- lp = lp.replace(/^https:\/\//, "");
1395
- } else if (lp.startsWith("http://")) {
1396
- lp = lp.replace(/^http:\/\//, "");
1397
- }
1398
- const parts = lp.split(":");
1399
- if (parts.length === 1) {
1400
- const port = parseInt(parts[0], 10);
1401
- if (!Number.isNaN(port) && isValidPort(port)) {
1402
- finalConfig.forwarding = `localhost:${port}`;
1403
- } else {
1404
- return new Error("Invalid local port");
1405
56
  }
1406
- } else if (parts.length === 2) {
1407
- const host = parts[0] || "localhost";
1408
- const port = parseInt(parts[1], 10);
1409
- if (!Number.isNaN(port) && isValidPort(port)) {
1410
- finalConfig.forwarding = `${host}:${port}`;
1411
- } else {
1412
- return new Error("Invalid local port. Please use -h option for help.");
1413
- }
1414
- } else {
1415
- return new Error("Invalid --localport format. Please use -h option for help.");
57
+ } catch {
1416
58
  }
1417
59
  return null;
1418
60
  }
1419
- function removeIPv6Brackets(ip) {
1420
- if (ip.startsWith("[") && ip.endsWith("]")) {
1421
- return ip.slice(1, -1);
1422
- }
1423
- return ip;
1424
- }
1425
- function ipv6SafeSplitColon(s) {
1426
- const result = [];
1427
- let buf = "";
1428
- const stack = [];
1429
- for (let i = 0; i < s.length; i++) {
1430
- const c = s[i];
1431
- if (c === "[") {
1432
- stack.push(c);
1433
- } else if (c === "]" && stack.length > 0) {
1434
- stack.pop();
1435
- }
1436
- if (c === ":" && stack.length === 0) {
1437
- result.push(buf);
1438
- buf = "";
1439
- } else {
1440
- buf += c;
1441
- }
61
+ function hasVCRedist() {
62
+ if (os.platform() !== "win32") {
63
+ return true;
1442
64
  }
1443
- result.push(buf);
1444
- return result;
1445
- }
1446
- var VALID_PROTOCOLS = ["http", "tcp", "udp", "tls"];
1447
- function parseDefaultForwarding(forwarding) {
1448
- const parts = ipv6SafeSplitColon(forwarding);
1449
- if (parts.length === 3) {
1450
- const remotePort = parseInt(parts[0], 10);
1451
- const localDomain = removeIPv6Brackets(parts[1] || "localhost");
1452
- const localPort = parseInt(parts[2], 10);
1453
- return { remotePort, localDomain, localPort };
65
+ if (checkRegistry()) {
66
+ return true;
1454
67
  }
1455
- if (parts.length === 4) {
1456
- const remoteDomain = removeIPv6Brackets(parts[0]);
1457
- const remotePort = parseInt(parts[1], 10);
1458
- const localDomain = removeIPv6Brackets(parts[2] || "localhost");
1459
- const localPort = parseInt(parts[3], 10);
1460
- return { remoteDomain, remotePort, localDomain, localPort };
68
+ if (checkDLLs()) {
69
+ return true;
1461
70
  }
1462
- return new Error("forwarding address incorrect");
71
+ return false;
1463
72
  }
1464
- function parseAdditionalForwarding(forwarding) {
1465
- const toPort = (v) => {
1466
- const n = parseInt(v, 10);
1467
- return Number.isNaN(n) ? null : n;
1468
- };
1469
- const validateDomain = (d) => d && domainRegex.test(d) ? d : null;
1470
- let protocol = "http";
1471
- let remoteDomainRaw;
1472
- const protocolsRequiringDomainPort = ["tcp", "udp"];
1473
- const lowForwarding = forwarding.toLowerCase();
1474
- let remaining = forwarding;
1475
- for (const p of VALID_PROTOCOLS) {
1476
- if (lowForwarding.startsWith(p + "//")) {
1477
- protocol = p;
1478
- remaining = forwarding.slice(p.length + 2);
1479
- break;
1480
- }
1481
- }
1482
- if (protocol === "http" && remaining === forwarding) {
1483
- const parts2 = ipv6SafeSplitColon(remaining);
1484
- if (parts2.length !== 4) {
1485
- return new Error(
1486
- "forwarding must be in format: domain:remotePort:localDomain:localPort"
1487
- );
1488
- }
1489
- const remoteDomain = validateDomain(removeIPv6Brackets(parts2[0]));
1490
- const localDomain2 = removeIPv6Brackets(parts2[2] || "localhost");
1491
- const localPort2 = toPort(parts2[3]);
1492
- if (!remoteDomain) {
1493
- return new Error("forwarding address incorrect: invalid domain");
1494
- }
1495
- if (localPort2 === null || !isValidPort(localPort2)) {
1496
- return new Error("forwarding address incorrect: invalid local port");
1497
- }
73
+ function getVCRedistStatus() {
74
+ if (os.platform() !== "win32") {
1498
75
  return {
1499
- protocol: "http",
1500
- remoteDomain,
1501
- remotePort: 0,
1502
- localDomain: localDomain2,
1503
- localPort: localPort2
76
+ required: false,
77
+ installed: true,
78
+ version: null,
79
+ method: "non-windows"
1504
80
  };
1505
81
  }
1506
- const domainPortMatch = remaining.match(/^([^:]+)\/(\d+):(.+)$/);
1507
- if (!domainPortMatch) {
1508
- return new Error(`forwarding must be in format: ${protocol}//domain/remotePort:localDomain:localPort`);
1509
- }
1510
- remoteDomainRaw = removeIPv6Brackets(domainPortMatch[1]);
1511
- const remotePortNum = toPort(domainPortMatch[2]);
1512
- const restParts = domainPortMatch[3];
1513
- if (!remoteDomainRaw || !domainRegex.test(remoteDomainRaw)) {
1514
- return new Error("forwarding address incorrect: invalid domain or remote port");
1515
- }
1516
- if (!remoteDomainRaw || remotePortNum === null || !isValidPort(remotePortNum)) {
1517
- return new Error(`${protocol} forwarding: invalid domain or port in format ${protocol}//domain/remotePort`);
1518
- }
1519
- const parts = ipv6SafeSplitColon(restParts);
1520
- if (parts.length !== 3) {
1521
- return new Error(`forwarding format incorrect: expected ${protocol}//domain/remotePort:placeholder:localDomain:localPort`);
1522
- }
1523
- const localDomain = removeIPv6Brackets(parts[1] || "localhost");
1524
- const localPort = toPort(parts[2]);
1525
- if (localPort === null || !isValidPort(localPort)) {
1526
- return new Error("forwarding address incorrect: invalid local port");
1527
- }
1528
- if (protocolsRequiringDomainPort.includes(protocol)) {
1529
- if (!remoteDomainRaw || !remotePortNum) {
1530
- return new Error(`${protocol} forwarding requires domain and port in format: ${protocol}//domain/remotePort:localDomain:localPort`);
1531
- }
1532
- }
82
+ const registryInstalled = checkRegistry();
83
+ const dllsPresent = checkDLLs();
84
+ const version = getVCRedistVersion();
1533
85
  return {
1534
- protocol,
1535
- remoteDomain: remoteDomainRaw,
1536
- remotePort: remotePortNum,
1537
- localDomain,
1538
- localPort
86
+ required: true,
87
+ installed: registryInstalled || dllsPresent,
88
+ version,
89
+ registryCheck: registryInstalled,
90
+ dllCheck: dllsPresent,
91
+ method: registryInstalled ? "registry" : dllsPresent ? "dll" : "none"
1539
92
  };
1540
93
  }
1541
- function parseReverseTunnelAddr(finalConfig, values) {
1542
- const reverseTunnel = values.R;
1543
- if ((!Array.isArray(reverseTunnel) || reverseTunnel.length === 0) && !values.localport && !finalConfig.forwarding) {
1544
- return new Error("local port not specified. Please use '-h' option for help.");
1545
- }
1546
- if (!Array.isArray(reverseTunnel) || reverseTunnel.length === 0) {
1547
- return null;
1548
- }
1549
- const forwarding = parseDefaultForwarding(reverseTunnel[0]);
1550
- if (forwarding instanceof Error) {
1551
- return forwarding;
1552
- }
1553
- finalConfig.forwarding = `${forwarding.localDomain}:${forwarding.localPort}`;
1554
- if (reverseTunnel.length > 1) {
1555
- finalConfig.additionalForwarding = [];
1556
- for (const t of reverseTunnel.slice(1)) {
1557
- const f = parseAdditionalForwarding(t);
1558
- if (f instanceof Error) {
1559
- return f;
1560
- }
1561
- finalConfig.additionalForwarding.push(f);
1562
- }
1563
- }
1564
- return null;
1565
- }
1566
- function parseLocalTunnelAddr(finalConfig, values) {
1567
- if (!Array.isArray(values.L) || values.L.length === 0) return null;
1568
- const firstL = values.L[0];
1569
- const parts = firstL.split(":");
1570
- if (parts.length === 3) {
1571
- const lp = parseInt(parts[2], 10);
1572
- if (!Number.isNaN(lp) && isValidPort(lp)) {
1573
- finalConfig.webDebugger = `localhost:${lp}`;
1574
- } else {
1575
- return new Error(`Invalid debugger port ${lp}`);
1576
- }
1577
- } else {
1578
- return new Error("Incorrect command line arguments: web debugger address incorrect. Please use '-h' option for help.");
1579
- }
1580
- }
1581
- function parseDebugger(finalConfig, values) {
1582
- let dbg = values.debugger;
1583
- if (typeof dbg !== "string") return;
1584
- dbg = dbg.startsWith(":") ? dbg.slice(1) : dbg;
1585
- const d = parseInt(dbg, 10);
1586
- if (!Number.isNaN(d) && isValidPort(d)) {
1587
- finalConfig.webDebugger = `localhost:${d}`;
1588
- } else {
1589
- logger.error("Invalid debugger port:", dbg);
1590
- return new Error(`Invalid debugger port ${dbg}. Please use '-h' option for help.`);
94
+ async function openDownloadPage() {
95
+ if (process.platform !== "win32") {
96
+ return;
1591
97
  }
1592
- }
1593
- function parseToken(finalConfig, explicitToken) {
1594
- if (typeof explicitToken === "string" && explicitToken) {
1595
- finalConfig.token = explicitToken;
1596
- }
1597
- }
1598
- function parseArgs(finalConfig, remainingPositionals) {
1599
- parseExtendedOptions(remainingPositionals, finalConfig);
1600
- }
1601
- function storeJson(config, saveconf) {
1602
- if (saveconf) {
1603
- const path3 = saveconf;
1604
- try {
1605
- fs.writeFileSync(path3, JSON.stringify(config, null, 2), { encoding: "utf-8", flag: "w" });
1606
- logger.info(`Configuration saved to ${path3}`);
1607
- } catch (err) {
1608
- const msg = err instanceof Error ? err.message : String(err);
1609
- logger.error("Error loading configuration:", msg);
1610
- }
1611
- }
1612
- }
1613
- function loadJsonConfig(config) {
1614
- const configpath = config["conf"];
1615
- if (typeof configpath === "string" && configpath.trim().length > 0) {
1616
- const filepath = path2.resolve(configpath);
1617
- try {
1618
- const data = fs.readFileSync(filepath, { encoding: "utf-8" });
1619
- const json = JSON.parse(data);
1620
- return json;
1621
- } catch (err) {
1622
- logger.error("Error loading configuration:", err);
1623
- }
98
+ const url = "https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170";
99
+ const command = `cmd.exe /c start "" "${url}"`;
100
+ try {
101
+ await execAsync(command);
102
+ printer_default.info("\nOpening Microsoft download page in your browser...");
103
+ printer_default.info(
104
+ "Please install the Visual C++ Runtime and restart this application.\n"
105
+ );
106
+ } catch (err) {
107
+ printer_default.info("\nUnable to open your browser automatically.");
108
+ printer_default.info(
109
+ "Please visit the following page to download the runtime:\n"
110
+ );
111
+ printer_default.info(url + "\n");
1624
112
  }
1625
- return null;
1626
113
  }
1627
- function isSaveConfOption(values) {
1628
- const saveconf = values["saveconf"];
1629
- if (typeof saveconf === "string" && saveconf.trim().length > 0) {
1630
- return saveconf;
114
+ function getVCRedistMessage() {
115
+ const status = getVCRedistStatus();
116
+ if (!status.required || status.installed) {
117
+ return null;
1631
118
  }
1632
- return null;
1633
- }
1634
- function parseServe(finalConfig, values) {
1635
- const sv = values.serve;
1636
- if (typeof sv !== "string" || sv.trim().length === 0) return null;
1637
- finalConfig.serve = sv;
1638
- return null;
1639
- }
1640
- async function buildFinalConfig(values, positionals) {
1641
- let token;
1642
- let server;
1643
- let type;
1644
- let forceFlag = false;
1645
- let qrCode = false;
1646
- let finalConfig = new Object();
1647
- let saveconf = isSaveConfOption(values);
1648
- const configFromFile = loadJsonConfig(values);
1649
- const userParse = parseUsers(positionals, values.token);
1650
- token = userParse.token;
1651
- server = userParse.server;
1652
- type = userParse.type;
1653
- forceFlag = userParse.forceFlag;
1654
- qrCode = userParse.qrCode;
1655
- const remainingPositionals = userParse.remaining;
1656
- const initialTunnel = type || values.type;
1657
- finalConfig = {
1658
- ...defaultOptions,
1659
- ...configFromFile || {},
1660
- // Apply loaded config on top of defaults
1661
- configid: getRandomId(),
1662
- token: token || (configFromFile?.token || (typeof values.token === "string" ? values.token : "")),
1663
- serverAddress: server || (configFromFile?.serverAddress || defaultOptions.serverAddress),
1664
- tunnelType: initialTunnel ? [initialTunnel] : configFromFile?.tunnelType || [TunnelType2.Http],
1665
- NoTUI: values.notui || (configFromFile?.NoTUI || false),
1666
- qrCode: qrCode || (configFromFile?.qrCode || false),
1667
- autoReconnect: values.autoreconnect || (configFromFile?.autoReconnect || false)
119
+ return {
120
+ error: true,
121
+ message: "Missing Microsoft Visual C++ Runtime. This application requires the Microsoft Visual C++ Runtime to run on Windows.\nPlease download and install it using the link below, then restart this application.\n"
1668
122
  };
1669
- parseType(finalConfig, values, type);
1670
- parseToken(finalConfig, token || values.token);
1671
- const dbgErr = parseDebugger(finalConfig, values);
1672
- if (dbgErr instanceof Error) throw dbgErr;
1673
- const lpErr = parseLocalPort(finalConfig, values);
1674
- if (lpErr instanceof Error) throw lpErr;
1675
- const rErr = parseReverseTunnelAddr(finalConfig, values);
1676
- if (rErr instanceof Error) throw rErr;
1677
- const lErr = parseLocalTunnelAddr(finalConfig, values);
1678
- if (lErr instanceof Error) throw lErr;
1679
- const serveErr = parseServe(finalConfig, values);
1680
- if (serveErr instanceof Error) throw serveErr;
1681
- if (forceFlag) finalConfig.force = true;
1682
- parseArgs(finalConfig, remainingPositionals);
1683
- storeJson(finalConfig, saveconf);
1684
- return finalConfig;
1685
123
  }
1686
124
 
1687
- // src/remote_management/remoteManagement.ts
1688
- import WebSocket from "ws";
1689
-
1690
- // src/types.ts
1691
- var ErrorCode = {
1692
- InvalidRequestMethodError: "INVALID_REQUEST_METHOD",
1693
- InvalidRequestBodyError: "COULD_NOT_READ_BODY",
1694
- InternalServerError: "INTERNAL_SERVER_ERROR",
1695
- InvalidBodyFormatError: "INVALID_DATA_FORMAT",
1696
- ErrorStartingTunnel: "ERROR_STARTING_TUNNEL",
1697
- TunnelNotFound: "TUNNEL_WITH_ID_OR_CONFIG_ID_NOT_FOUND",
1698
- TunnelAlreadyRunningError: "TUNNEL_WITH_ID_OR_CONFIG_ID_ALREADY_RUNNING",
1699
- WebsocketUpgradeFailError: "WEBSOCKET_UPGRADE_FAILED",
1700
- RemoteManagementAlreadyRunning: "REMOTE_MANAGEMENT_ALREADY_RUNNING",
1701
- RemoteManagementNotRunning: "REMOTE_MANAGEMENT_NOT_RUNNING",
1702
- RemoteManagementDeserializationFailed: "REMOTE_MANAGEMENT_DESERIALIZATION_FAILED"
1703
- };
1704
- function isErrorResponse(obj) {
1705
- return typeof obj === "object" && obj !== null && "code" in obj && "message" in obj && typeof obj.message === "string" && Object.values(ErrorCode).includes(obj.code);
1706
- }
1707
- function newErrorResponse(codeOrError, message) {
1708
- if (typeof codeOrError === "object") {
1709
- return codeOrError;
125
+ // src/index.ts
126
+ async function verifyAndLoad() {
127
+ if (process.platform === "win32" && !hasVCRedist()) {
128
+ const msg = getVCRedistMessage();
129
+ printer_default.warn(
130
+ msg?.message ?? "This application requires the Microsoft Visual C++ Runtime on Windows."
131
+ );
132
+ await openDownloadPage();
133
+ process.exit(1);
1710
134
  }
1711
- return {
1712
- code: codeOrError,
1713
- message
1714
- };
1715
- }
1716
- function NewResponseObject(data) {
1717
- const encoder = new TextEncoder();
1718
- const bytes = encoder.encode(JSON.stringify(data));
1719
- return {
1720
- response: bytes,
1721
- requestid: "",
1722
- command: "",
1723
- error: false,
1724
- errorresponse: {}
1725
- };
135
+ await import("./main-CZY6GID4.js");
1726
136
  }
1727
- function NewErrorResponseObject(errorResponse) {
1728
- return {
1729
- response: new Uint8Array(),
1730
- requestid: "",
1731
- command: "",
1732
- error: true,
1733
- errorresponse: errorResponse
1734
- };
1735
- }
1736
- function newStatus(tunnelState, errorCode, errorMsg) {
1737
- let assignedState = tunnelState;
1738
- if (tunnelState === "live" /* Live */) {
1739
- assignedState = "running" /* Running */;
1740
- } else if (tunnelState === "idle" /* New */) {
1741
- assignedState = "idle" /* New */;
1742
- } else if (tunnelState === "closed" /* Closed */) {
1743
- assignedState = "exited" /* Exited */;
1744
- }
1745
- const now = (/* @__PURE__ */ new Date()).toISOString();
1746
- return {
1747
- state: assignedState,
1748
- errorcode: errorCode,
1749
- errormsg: errorMsg,
1750
- createdtimestamp: now,
1751
- starttimestamp: now,
1752
- endtimestamp: now,
1753
- warnings: []
1754
- };
1755
- }
1756
- function newStats() {
1757
- return {
1758
- numLiveConnections: 0,
1759
- numTotalConnections: 0,
1760
- numTotalReqBytes: 0,
1761
- numTotalResBytes: 0,
1762
- numTotalTxBytes: 0,
1763
- elapsedTime: 0
1764
- };
1765
- }
1766
- var RemoteManagementStatus = {
1767
- Connecting: "CONNECTING",
1768
- Disconnecting: "DISCONNECTING",
1769
- Reconnecting: "RECONNECTING",
1770
- Running: "RUNNING",
1771
- NotRunning: "NOT_RUNNING",
1772
- Error: "ERROR"
1773
- };
1774
-
1775
- // src/remote_management/remote_schema.ts
1776
- import { TunnelType as TunnelType3 } from "@pinggy/pinggy";
1777
- import { z } from "zod";
1778
- var HeaderModificationSchema = z.object({
1779
- key: z.string(),
1780
- value: z.array(z.string()).optional(),
1781
- type: z.enum(["add", "remove", "update"])
1782
- });
1783
- var AdditionalForwardingSchema = z.object({
1784
- remoteDomain: z.string().optional(),
1785
- remotePort: z.number().optional(),
1786
- localDomain: z.string(),
1787
- localPort: z.number()
137
+ verifyAndLoad().catch((err) => {
138
+ printer_default.error(`Failed to start CLI:, ${err}`);
139
+ process.exit(1);
1788
140
  });
1789
- var TunnelConfigSchema = z.object({
1790
- allowPreflight: z.boolean().optional(),
1791
- // primary key
1792
- allowpreflight: z.boolean().optional(),
1793
- // legacy key
1794
- autoreconnect: z.boolean(),
1795
- basicauth: z.array(z.object({ username: z.string(), password: z.string() })).nullable(),
1796
- bearerauth: z.string().nullable(),
1797
- configid: z.string(),
1798
- configname: z.string(),
1799
- greetmsg: z.string().optional(),
1800
- force: z.boolean(),
1801
- forwardedhost: z.string(),
1802
- fullRequestUrl: z.boolean(),
1803
- headermodification: z.array(HeaderModificationSchema),
1804
- httpsOnly: z.boolean(),
1805
- internalwebdebuggerport: z.number(),
1806
- ipwhitelist: z.array(z.string()).nullable(),
1807
- localport: z.number(),
1808
- localsservertls: z.union([z.boolean(), z.string()]),
1809
- localservertlssni: z.string().nullable(),
1810
- regioncode: z.string(),
1811
- noReverseProxy: z.boolean(),
1812
- serveraddress: z.string(),
1813
- serverport: z.number(),
1814
- statusCheckInterval: z.number(),
1815
- token: z.string(),
1816
- tunnelTimeout: z.number(),
1817
- type: z.enum([
1818
- TunnelType3.Http,
1819
- TunnelType3.Tcp,
1820
- TunnelType3.Udp,
1821
- TunnelType3.Tls,
1822
- TunnelType3.TlsTcp
1823
- ]),
1824
- webdebuggerport: z.number(),
1825
- xff: z.string(),
1826
- additionalForwarding: z.array(AdditionalForwardingSchema).optional(),
1827
- serve: z.string().optional()
1828
- }).superRefine((data, ctx) => {
1829
- if (data.allowPreflight === void 0 && data.allowpreflight === void 0) {
1830
- ctx.addIssue({
1831
- code: "custom",
1832
- message: "Either allowPreflight or allowpreflight is required",
1833
- path: ["allowPreflight"]
1834
- });
1835
- }
1836
- }).transform((data) => ({
1837
- ...data,
1838
- allowPreflight: data.allowPreflight ?? data.allowpreflight,
1839
- allowpreflight: data.allowPreflight ?? data.allowpreflight
1840
- }));
1841
- var StartSchema = z.object({
1842
- tunnelID: z.string().nullable().optional(),
1843
- tunnelConfig: TunnelConfigSchema
1844
- });
1845
- var StopSchema = z.object({
1846
- tunnelID: z.string().min(1)
1847
- });
1848
- var GetSchema = StopSchema;
1849
- var RestartSchema = StopSchema;
1850
- var UpdateConfigSchema = z.object({
1851
- tunnelConfig: TunnelConfigSchema
1852
- });
1853
- function tunnelConfigToPinggyOptions(config) {
1854
- return {
1855
- token: config.token || "",
1856
- serverAddress: config.serveraddress || "free.pinggy.io",
1857
- forwarding: `${config.forwardedhost || "localhost"}:${config.localport}`,
1858
- webDebugger: config.webdebuggerport ? `localhost:${config.webdebuggerport}` : "",
1859
- ipWhitelist: config.ipwhitelist || [],
1860
- basicAuth: config.basicauth ? config.basicauth : [],
1861
- bearerTokenAuth: config.bearerauth ? [config.bearerauth] : [],
1862
- headerModification: config.headermodification,
1863
- xForwardedFor: !!config.xff,
1864
- httpsOnly: config.httpsOnly,
1865
- originalRequestUrl: config.fullRequestUrl,
1866
- allowPreflight: config.allowPreflight,
1867
- reverseProxy: config.noReverseProxy,
1868
- force: config.force,
1869
- autoReconnect: config.autoreconnect,
1870
- optional: {
1871
- sniServerName: config.localservertlssni || ""
1872
- }
1873
- };
1874
- }
1875
- function pinggyOptionsToTunnelConfig(opts, configid, configName, localserverTls, greetMsg, additionalForwarding, serve) {
1876
- const forwarding = Array.isArray(opts.forwarding) ? String(opts.forwarding[0].address).replace("//", "").replace(/\/$/, "") : String(opts.forwarding).replace("//", "").replace(/\/$/, "");
1877
- const parsedForwardedHost = forwarding.split(":").length == 3 ? forwarding.split(":")[1] : forwarding.split(":")[0];
1878
- const parsedLocalPort = forwarding.split(":").length == 3 ? parseInt(forwarding.split(":")[2], 10) : parseInt(forwarding.split(":")[1], 10);
1879
- const tunnelType = (Array.isArray(opts.forwarding) ? opts.forwarding[0]?.type : void 0) ?? TunnelType3.Http;
1880
- const parsedTokens = opts.bearerTokenAuth ? Array.isArray(opts.bearerTokenAuth) ? opts.bearerTokenAuth : JSON.parse(opts.bearerTokenAuth) : [];
1881
- return {
1882
- allowPreflight: opts.allowPreflight ?? false,
1883
- allowpreflight: opts.allowPreflight ?? false,
1884
- autoreconnect: opts.autoReconnect ?? false,
1885
- basicauth: opts.basicAuth && Object.keys(opts.basicAuth).length ? opts.basicAuth : null,
1886
- bearerauth: parsedTokens.length ? parsedTokens.join(",") : null,
1887
- configid,
1888
- configname: configName,
1889
- greetmsg: greetMsg || "",
1890
- force: opts.force ?? false,
1891
- forwardedhost: parsedForwardedHost || "localhost",
1892
- fullRequestUrl: opts.originalRequestUrl ?? false,
1893
- headermodification: opts.headerModification || [],
1894
- //structured list
1895
- httpsOnly: opts.httpsOnly ?? false,
1896
- internalwebdebuggerport: 0,
1897
- ipwhitelist: opts.ipWhitelist ? Array.isArray(opts.ipWhitelist) ? opts.ipWhitelist : JSON.parse(opts.ipWhitelist) : null,
1898
- localport: parsedLocalPort || 0,
1899
- localservertlssni: null,
1900
- regioncode: "",
1901
- noReverseProxy: opts.reverseProxy ?? false,
1902
- serveraddress: opts.serverAddress || "free.pinggy.io",
1903
- serverport: 0,
1904
- statusCheckInterval: 0,
1905
- token: opts.token || "",
1906
- tunnelTimeout: 0,
1907
- type: tunnelType,
1908
- webdebuggerport: Number(opts.webDebugger?.split(":")[0]) || 0,
1909
- xff: opts.xForwardedFor ? "1" : "",
1910
- localsservertls: localserverTls || false,
1911
- additionalForwarding: additionalForwarding || [],
1912
- serve: serve || ""
1913
- };
1914
- }
1915
-
1916
- // src/remote_management/handler.ts
1917
- import { TunnelType as TunnelType4 } from "@pinggy/pinggy";
1918
- var TunnelOperations = class {
1919
- constructor() {
1920
- this.tunnelManager = TunnelManager.getInstance();
1921
- }
1922
- buildStatus(tunnelId, state, errorCode) {
1923
- const status = newStatus(state, errorCode, "");
1924
- try {
1925
- const managed = this.tunnelManager.getManagedTunnel("", tunnelId);
1926
- if (managed) {
1927
- status.createdtimestamp = managed.createdAt || "";
1928
- status.starttimestamp = managed.startedAt || "";
1929
- status.endtimestamp = managed.stoppedAt || "";
1930
- }
1931
- } catch (e) {
1932
- }
1933
- return status;
1934
- }
1935
- // --- Helper to construct TunnelResponse ---
1936
- async buildTunnelResponse(tunnelid, tunnelConfig, configid, tunnelName, additionalForwarding, serve) {
1937
- const [status, stats, tlsInfo, greetMsg, remoteurls] = await Promise.all([
1938
- this.tunnelManager.getTunnelStatus(tunnelid),
1939
- this.tunnelManager.getLatestTunnelStats(tunnelid) || newStats(),
1940
- this.tunnelManager.getLocalserverTlsInfo(tunnelid),
1941
- this.tunnelManager.getTunnelGreetMessage(tunnelid),
1942
- this.tunnelManager.getTunnelUrls(tunnelid)
1943
- ]);
1944
- return {
1945
- tunnelid,
1946
- remoteurls,
1947
- tunnelconfig: pinggyOptionsToTunnelConfig(tunnelConfig, configid, tunnelName, tlsInfo, greetMsg, additionalForwarding),
1948
- status: this.buildStatus(tunnelid, status, "" /* NoError */),
1949
- stats
1950
- };
1951
- }
1952
- error(code, err, fallback) {
1953
- return newErrorResponse({
1954
- code,
1955
- message: err instanceof Error ? err.message : fallback
1956
- });
1957
- }
1958
- // --- Operations ---
1959
- async handleStart(config) {
1960
- try {
1961
- const opts = tunnelConfigToPinggyOptions(config);
1962
- const additionalForwardingParsed = config.additionalForwarding || [];
1963
- const { tunnelid, instance, tunnelName, additionalForwarding, serve } = await this.tunnelManager.createTunnel({
1964
- ...opts,
1965
- tunnelType: Array.isArray(config.type) ? config.type : config.type ? [config.type] : [TunnelType4.Http],
1966
- // Temporary fix in future we will not use this field.
1967
- configid: config.configid,
1968
- tunnelName: config.configname,
1969
- additionalForwarding: additionalForwardingParsed,
1970
- serve: config.serve
1971
- });
1972
- this.tunnelManager.startTunnel(tunnelid);
1973
- const tunnelPconfig = await this.tunnelManager.getTunnelConfig("", tunnelid);
1974
- const resp = this.buildTunnelResponse(tunnelid, tunnelPconfig, config.configid, tunnelName, additionalForwarding, serve);
1975
- return resp;
1976
- } catch (err) {
1977
- return this.error(ErrorCode.ErrorStartingTunnel, err, "Unknown error occurred while starting tunnel");
1978
- }
1979
- }
1980
- async handleUpdateConfig(config) {
1981
- try {
1982
- const opts = tunnelConfigToPinggyOptions(config);
1983
- const tunnel = await this.tunnelManager.updateConfig({
1984
- ...opts,
1985
- tunnelType: Array.isArray(config.type) ? config.type : config.type ? [config.type] : [TunnelType4.Http],
1986
- // // Temporary fix in future we will not use this field.
1987
- configid: config.configid,
1988
- tunnelName: config.configname,
1989
- additionalForwarding: config.additionalForwarding || [],
1990
- serve: config.serve
1991
- });
1992
- if (!tunnel.instance || !tunnel.tunnelConfig)
1993
- throw new Error("Invalid tunnel state after configuration update");
1994
- return this.buildTunnelResponse(tunnel.tunnelid, tunnel.tunnelConfig, config.configid, tunnel.tunnelName, tunnel.additionalForwarding, tunnel.serve);
1995
- } catch (err) {
1996
- return this.error(ErrorCode.InternalServerError, err, "Failed to update tunnel configuration");
1997
- }
1998
- }
1999
- async handleList() {
2000
- try {
2001
- const tunnels = await this.tunnelManager.getAllTunnels();
2002
- if (tunnels.length === 0) {
2003
- return [];
2004
- }
2005
- return Promise.all(
2006
- tunnels.map(async (t) => {
2007
- const rawStats = this.tunnelManager.getLatestTunnelStats(t.tunnelid) || newStats();
2008
- const [status, tlsInfo, greetMsg] = await Promise.all([
2009
- this.tunnelManager.getTunnelStatus(t.tunnelid),
2010
- this.tunnelManager.getLocalserverTlsInfo(t.tunnelid),
2011
- this.tunnelManager.getTunnelGreetMessage(t.tunnelid)
2012
- ]);
2013
- const pinggyOptions = status !== "closed" /* Closed */ && status !== "exited" /* Exited */ ? await this.tunnelManager.getTunnelConfig("", t.tunnelid) : t.tunnelConfig;
2014
- const tunnelConfig = pinggyOptionsToTunnelConfig(pinggyOptions, t.configid, t.tunnelName, tlsInfo, greetMsg, t.additionalForwarding, t.serve);
2015
- return {
2016
- tunnelid: t.tunnelid,
2017
- remoteurls: t.remoteurls,
2018
- status: this.buildStatus(t.tunnelid, status, "" /* NoError */),
2019
- stats: rawStats,
2020
- tunnelconfig: tunnelConfig
2021
- };
2022
- })
2023
- );
2024
- } catch (err) {
2025
- return this.error(ErrorCode.InternalServerError, err, "Failed to list tunnels");
2026
- }
2027
- }
2028
- async handleStop(tunnelid) {
2029
- try {
2030
- const { configid } = this.tunnelManager.stopTunnel(tunnelid);
2031
- const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
2032
- if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
2033
- return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, configid, managed.tunnelName, managed.additionalForwarding, managed.serve);
2034
- } catch (err) {
2035
- return this.error(ErrorCode.TunnelNotFound, err, "Failed to stop tunnel");
2036
- }
2037
- }
2038
- async handleGet(tunnelid) {
2039
- try {
2040
- const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
2041
- if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
2042
- return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, managed.configid, managed.tunnelName, managed.additionalForwarding, managed.serve);
2043
- } catch (err) {
2044
- return this.error(ErrorCode.TunnelNotFound, err, "Failed to get tunnel information");
2045
- }
2046
- }
2047
- async handleRestart(tunnelid) {
2048
- try {
2049
- await this.tunnelManager.restartTunnel(tunnelid);
2050
- const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
2051
- if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
2052
- return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, managed.configid, managed.tunnelName, managed.additionalForwarding, managed.serve);
2053
- } catch (err) {
2054
- return this.error(ErrorCode.TunnelNotFound, err, "Failed to restart tunnel");
2055
- }
2056
- }
2057
- handleRegisterStatsListener(tunnelid, listener) {
2058
- this.tunnelManager.registerStatsListener(tunnelid, listener);
2059
- }
2060
- handleUnregisterStatsListener(tunnelid, listnerId) {
2061
- this.tunnelManager.deregisterStatsListener(tunnelid, listnerId);
2062
- }
2063
- handleGetTunnelStats(tunnelid) {
2064
- try {
2065
- const stats = this.tunnelManager.getTunnelStats(tunnelid);
2066
- if (!stats) {
2067
- return [newStats()];
2068
- }
2069
- return stats;
2070
- } catch (err) {
2071
- return this.error(ErrorCode.TunnelNotFound, err, "Failed to get tunnel stats");
2072
- }
2073
- }
2074
- handleRegisterDisconnectListener(tunnelid, listener) {
2075
- this.tunnelManager.registerDisconnectListener(tunnelid, listener);
2076
- }
2077
- handleRemoveStoppedTunnelByConfigId(configId) {
2078
- try {
2079
- return this.tunnelManager.removeStoppedTunnelByConfigId(configId);
2080
- } catch (err) {
2081
- return this.error(ErrorCode.InternalServerError, err, "Failed to remove stopped tunnel by configId");
2082
- }
2083
- }
2084
- handleRemoveStoppedTunnelByTunnelId(tunnelId) {
2085
- try {
2086
- return this.tunnelManager.removeStoppedTunnelByTunnelId(tunnelId);
2087
- } catch (err) {
2088
- return this.error(ErrorCode.InternalServerError, err, "Failed to remove stopped tunnel by tunnelId");
2089
- }
2090
- }
2091
- };
2092
-
2093
- // src/remote_management/websocket_handlers.ts
2094
- import z2 from "zod";
2095
- var WebSocketCommandHandler = class {
2096
- constructor() {
2097
- this.tunnelHandler = new TunnelOperations();
2098
- }
2099
- safeParse(text) {
2100
- if (!text) return void 0;
2101
- try {
2102
- return JSON.parse(text);
2103
- } catch (e) {
2104
- logger.warn("Invalid JSON payload", { error: String(e), text });
2105
- return void 0;
2106
- }
2107
- }
2108
- sendResponse(ws, resp) {
2109
- const payload = {
2110
- ...resp,
2111
- response: Buffer.from(resp.response || []).toString("base64")
2112
- };
2113
- ws.send(JSON.stringify(payload));
2114
- }
2115
- sendError(ws, req, message, code = ErrorCode.InternalServerError) {
2116
- const resp = NewErrorResponseObject({ code, message });
2117
- resp.command = req.command || "";
2118
- resp.requestid = req.requestid || "";
2119
- this.sendResponse(ws, resp);
2120
- }
2121
- async handleStartReq(req, raw) {
2122
- const dc = StartSchema.parse(raw);
2123
- printer_default.info("Starting tunnel with config name: " + dc.tunnelConfig.configname);
2124
- const result = await this.tunnelHandler.handleStart(dc.tunnelConfig);
2125
- return this.wrapResponse(result, req);
2126
- }
2127
- async handleStopReq(req, raw) {
2128
- const dc = StopSchema.parse(raw);
2129
- printer_default.info("Stopping tunnel with ID: " + dc.tunnelID);
2130
- const result = await this.tunnelHandler.handleStop(dc.tunnelID);
2131
- return this.wrapResponse(result, req);
2132
- }
2133
- async handleGetReq(req, raw) {
2134
- const dc = GetSchema.parse(raw);
2135
- const result = await this.tunnelHandler.handleGet(dc.tunnelID);
2136
- return this.wrapResponse(result, req);
2137
- }
2138
- async handleRestartReq(req, raw) {
2139
- const dc = RestartSchema.parse(raw);
2140
- const result = await this.tunnelHandler.handleRestart(dc.tunnelID);
2141
- return this.wrapResponse(result, req);
2142
- }
2143
- async handleUpdateConfigReq(req, raw) {
2144
- const dc = UpdateConfigSchema.parse(raw);
2145
- const result = await this.tunnelHandler.handleUpdateConfig(dc.tunnelConfig);
2146
- return this.wrapResponse(result, req);
2147
- }
2148
- async handleListReq(req) {
2149
- const result = await this.tunnelHandler.handleList();
2150
- return this.wrapResponse(result, req);
2151
- }
2152
- wrapResponse(result, req) {
2153
- if (isErrorResponse(result)) {
2154
- const errResp = NewErrorResponseObject(result);
2155
- errResp.command = req.command;
2156
- errResp.requestid = req.requestid;
2157
- return errResp;
2158
- }
2159
- const finalResult = JSON.parse(JSON.stringify(result));
2160
- if (Array.isArray(finalResult)) {
2161
- finalResult.forEach((item) => {
2162
- if (item?.tunnelconfig) {
2163
- delete item.tunnelconfig.allowPreflight;
2164
- }
2165
- });
2166
- } else if (finalResult?.tunnelconfig) {
2167
- delete finalResult.tunnelconfig.allowPreflight;
2168
- }
2169
- const respObj = NewResponseObject(finalResult);
2170
- respObj.command = req.command;
2171
- respObj.requestid = req.requestid;
2172
- return respObj;
2173
- }
2174
- async handle(ws, req) {
2175
- const cmd = (req.command || "").toLowerCase();
2176
- const raw = this.safeParse(req.data);
2177
- try {
2178
- let response;
2179
- switch (cmd) {
2180
- case "start": {
2181
- response = await this.handleStartReq(req, raw);
2182
- break;
2183
- }
2184
- case "stop": {
2185
- response = await this.handleStopReq(req, raw);
2186
- break;
2187
- }
2188
- case "get": {
2189
- response = await this.handleGetReq(req, raw);
2190
- break;
2191
- }
2192
- case "restart": {
2193
- response = await this.handleRestartReq(req, raw);
2194
- break;
2195
- }
2196
- case "updateconfig": {
2197
- response = await this.handleUpdateConfigReq(req, raw);
2198
- break;
2199
- }
2200
- case "list": {
2201
- response = await this.handleListReq(req);
2202
- break;
2203
- }
2204
- default:
2205
- if (typeof req.command === "string") {
2206
- logger.warn("Unknown command", { command: req.command });
2207
- }
2208
- return this.sendError(ws, req, "Invalid command");
2209
- }
2210
- logger.debug("Sending response", { command: response.command, requestid: response.requestid });
2211
- this.sendResponse(ws, response);
2212
- } catch (e) {
2213
- if (e instanceof z2.ZodError) {
2214
- logger.warn("Validation failed", { cmd, issues: e.issues });
2215
- return this.sendError(ws, req, "Invalid request data", ErrorCode.InvalidBodyFormatError);
2216
- }
2217
- logger.error("Error handling command", { cmd, error: String(e) });
2218
- return this.sendError(ws, req, e?.message || "Internal error");
2219
- }
2220
- }
2221
- };
2222
- function handleConnectionStatusMessage(firstMessage) {
2223
- try {
2224
- const text = typeof firstMessage === "string" ? firstMessage : firstMessage.toString();
2225
- const cs = JSON.parse(text);
2226
- if (!cs.success) {
2227
- const msg = cs.error_msg || "Connection failed";
2228
- printer_default.warn(`Connection failed: ${msg}`);
2229
- logger.warn("Remote management connection failed", { error_code: cs.error_code, error_msg: msg });
2230
- return false;
2231
- }
2232
- return true;
2233
- } catch (e) {
2234
- logger.warn("Failed to parse connection status message", { error: String(e) });
2235
- return true;
2236
- }
2237
- }
2238
-
2239
- // src/remote_management/remoteManagement.ts
2240
- var RECONNECT_SLEEP_MS = 5e3;
2241
- var PING_INTERVAL_MS = 3e4;
2242
- var _remoteManagementState = {
2243
- status: "NOT_RUNNING",
2244
- errorMessage: ""
2245
- };
2246
- var _stopRequested = false;
2247
- var currentWs = null;
2248
- function buildRemoteManagementWsUrl(manage) {
2249
- let baseUrl = (manage || "dashboard.pinggy.io").trim();
2250
- if (!(baseUrl.startsWith("ws://") || baseUrl.startsWith("wss://"))) {
2251
- baseUrl = "wss://" + baseUrl;
2252
- }
2253
- const trimmed = baseUrl.replace(/\/$/, "");
2254
- return `${trimmed}/backend/api/v1/remote-management/connect`;
2255
- }
2256
- function extractHostname(u) {
2257
- try {
2258
- const url = new URL(u);
2259
- return url.host;
2260
- } catch {
2261
- return u;
2262
- }
2263
- }
2264
- function sleep(ms) {
2265
- return new Promise((res) => setTimeout(res, ms));
2266
- }
2267
- async function parseRemoteManagement(values) {
2268
- const rmToken = values["remote-management"];
2269
- if (typeof rmToken === "string" && rmToken.trim().length > 0) {
2270
- const manageHost = values["manage"];
2271
- try {
2272
- await initiateRemoteManagement(rmToken, manageHost);
2273
- return { ok: true };
2274
- } catch (e) {
2275
- logger.error("Failed to initiate remote management:", e);
2276
- return { ok: false, error: e };
2277
- }
2278
- }
2279
- }
2280
- async function initiateRemoteManagement(token, manage) {
2281
- if (!token || token.trim().length === 0) {
2282
- throw new Error("Remote management token is required (use --remote-management <TOKEN>)");
2283
- }
2284
- const wsUrl = buildRemoteManagementWsUrl(manage);
2285
- const wsHost = extractHostname(wsUrl);
2286
- logger.info("Remote management mode enabled.");
2287
- _stopRequested = false;
2288
- const sigintHandler = () => {
2289
- _stopRequested = true;
2290
- };
2291
- process.once("SIGINT", sigintHandler);
2292
- const logConnecting = () => {
2293
- printer_default.print(`Connecting to ${wsHost}`);
2294
- logger.info("Connecting to remote management", { wsUrl });
2295
- };
2296
- while (!_stopRequested) {
2297
- logConnecting();
2298
- setRemoteManagementState({ status: RemoteManagementStatus.Connecting, errorMessage: "" });
2299
- try {
2300
- await handleWebSocketConnection(wsUrl, wsHost, token);
2301
- } catch (error) {
2302
- logger.warn("Remote management connection error", { error: String(error) });
2303
- }
2304
- if (_stopRequested) break;
2305
- printer_default.warn(`Remote management disconnected. Reconnecting in ${RECONNECT_SLEEP_MS / 1e3} seconds...`);
2306
- logger.info("Reconnecting to remote management after disconnect");
2307
- await sleep(RECONNECT_SLEEP_MS);
2308
- }
2309
- process.removeListener("SIGINT", sigintHandler);
2310
- logger.info("Remote management stopped.");
2311
- return getRemoteManagementState();
2312
- }
2313
- async function handleWebSocketConnection(wsUrl, wsHost, token) {
2314
- return new Promise((resolve) => {
2315
- const ws = new WebSocket(wsUrl, {
2316
- headers: { Authorization: `Bearer ${token}` }
2317
- });
2318
- currentWs = ws;
2319
- let heartbeat = null;
2320
- let firstMessage = true;
2321
- const cleanup = () => {
2322
- if (heartbeat) clearInterval(heartbeat);
2323
- currentWs = null;
2324
- resolve();
2325
- };
2326
- ws.once("open", () => {
2327
- printer_default.success(`Connected to ${wsHost}`);
2328
- heartbeat = setInterval(() => {
2329
- if (ws.readyState === WebSocket.OPEN) ws.ping();
2330
- }, PING_INTERVAL_MS);
2331
- });
2332
- ws.on("ping", () => ws.pong());
2333
- ws.on("message", async (data) => {
2334
- try {
2335
- if (firstMessage) {
2336
- firstMessage = false;
2337
- const ok = handleConnectionStatusMessage(data);
2338
- if (!ok) ws.close();
2339
- return;
2340
- }
2341
- setRemoteManagementState({ status: RemoteManagementStatus.Running, errorMessage: "" });
2342
- const req = JSON.parse(data.toString("utf8"));
2343
- await new WebSocketCommandHandler().handle(ws, req);
2344
- } catch (e) {
2345
- logger.warn("Failed handling websocket message", { error: String(e) });
2346
- }
2347
- });
2348
- ws.on("unexpected-response", (_, res) => {
2349
- setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: `HTTP ${res.statusCode}` });
2350
- if (res.statusCode === 401) {
2351
- printer_default.error("Unauthorized. Please enter a valid token.");
2352
- logger.error("Unauthorized (401) on remote management connect");
2353
- } else {
2354
- printer_default.warn(`Unexpected HTTP ${res.statusCode}. Retrying...`);
2355
- logger.warn("Unexpected HTTP response", { statusCode: res.statusCode });
2356
- }
2357
- ws.close();
2358
- });
2359
- ws.on("close", (code, reason) => {
2360
- setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: "" });
2361
- logger.info("WebSocket closed", { code, reason: reason.toString() });
2362
- printer_default.warn(`Disconnected (code: ${code}). Retrying...`);
2363
- cleanup();
2364
- });
2365
- ws.on("error", (err) => {
2366
- setRemoteManagementState({ status: RemoteManagementStatus.Error, errorMessage: err.message });
2367
- logger.warn("WebSocket error", { error: err.message });
2368
- printer_default.error(err);
2369
- cleanup();
2370
- });
2371
- });
2372
- }
2373
- async function closeRemoteManagement(timeoutMs = 1e4) {
2374
- _stopRequested = true;
2375
- try {
2376
- if (currentWs) {
2377
- try {
2378
- setRemoteManagementState({ status: RemoteManagementStatus.Disconnecting, errorMessage: "" });
2379
- currentWs.close();
2380
- } catch (e) {
2381
- logger.warn("Error while closing current remote management websocket", { error: String(e) });
2382
- }
2383
- }
2384
- const start = Date.now();
2385
- while (_remoteManagementState.status === "RUNNING") {
2386
- if (Date.now() - start > timeoutMs) {
2387
- logger.warn("Timed out waiting for remote management to stop");
2388
- break;
2389
- }
2390
- await sleep(200);
2391
- }
2392
- } finally {
2393
- currentWs = null;
2394
- setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: "" });
2395
- return getRemoteManagementState();
2396
- }
2397
- }
2398
- function getRemoteManagementState() {
2399
- return _remoteManagementState;
2400
- }
2401
- function setRemoteManagementState(state, errorMessage) {
2402
- _remoteManagementState = {
2403
- status: state.status,
2404
- errorMessage: errorMessage || ""
2405
- };
2406
- }
2407
-
2408
- // src/utils/parseArgs.ts
2409
- import { parseArgs as parseArgs2 } from "util";
2410
- import * as os from "os";
2411
- function isInlineColonFlag(arg) {
2412
- return /^-([RL])[A-Za-z0-9._-]*:?$/.test(arg);
2413
- }
2414
- function preprocessWindowsArgs(args) {
2415
- if (os.platform() !== "win32") return args;
2416
- const out = [];
2417
- let i = 0;
2418
- while (i < args.length) {
2419
- const arg = args[i];
2420
- if (isInlineColonFlag(arg)) {
2421
- if (i + 1 < args.length && !args[i + 1].startsWith("-")) {
2422
- let merged = arg + args[i + 1];
2423
- i += 2;
2424
- out.push(merged);
2425
- continue;
2426
- }
2427
- out.push(arg);
2428
- i++;
2429
- continue;
2430
- }
2431
- out.push(arg);
2432
- i++;
2433
- }
2434
- return out;
2435
- }
2436
- function parseCliArgs(options) {
2437
- const rawArgs = process.argv.slice(2);
2438
- const processedArgs = preprocessWindowsArgs(rawArgs);
2439
- const parsed = parseArgs2({
2440
- args: processedArgs,
2441
- options,
2442
- allowPositionals: true
2443
- });
2444
- const hasAnyArgs = parsed.positionals.length > 0 || Object.values(parsed.values).some((v) => v !== void 0 && v !== false);
2445
- return {
2446
- ...parsed,
2447
- hasAnyArgs
2448
- };
2449
- }
2450
-
2451
- // src/utils/getFreePort.ts
2452
- import net from "net";
2453
- function getFreePort(webDebugger) {
2454
- return new Promise((resolve, reject) => {
2455
- const tryPort = (portToTry) => {
2456
- const server = net.createServer();
2457
- server.unref();
2458
- server.on("error", (err) => {
2459
- if (portToTry !== 0) {
2460
- tryPort(0);
2461
- } else {
2462
- reject(err);
2463
- }
2464
- });
2465
- server.listen(portToTry, () => {
2466
- const address = server.address();
2467
- const port = address ? address.port : 0;
2468
- server.close(() => resolve(port));
2469
- });
2470
- };
2471
- let providedPort = 0;
2472
- if (webDebugger && webDebugger.includes(":")) {
2473
- const portPart = webDebugger.split(":")[1];
2474
- const parsed = parseInt(portPart, 10);
2475
- if (!isNaN(parsed) && parsed > 0 && parsed < 65536) {
2476
- providedPort = parsed;
2477
- }
2478
- }
2479
- tryPort(providedPort);
2480
- });
2481
- }
2482
-
2483
- // src/cli/starCli.ts
2484
- import pico3 from "picocolors";
2485
-
2486
- // src/tui/blessed/TunnelTui.ts
2487
- import blessed3 from "blessed";
2488
-
2489
- // src/tui/blessed/qrCodeGenerator.ts
2490
- import QRCode from "qrcode";
2491
- async function createQrCodes(urls) {
2492
- const codes = [];
2493
- for (const url of urls) {
2494
- const qr = await QRCode.toString(url, {
2495
- type: "terminal",
2496
- small: true,
2497
- margin: 0,
2498
- errorCorrectionLevel: "L"
2499
- });
2500
- codes.push(qr);
2501
- }
2502
- return codes;
2503
- }
2504
-
2505
- // src/tui/blessed/webDebuggerConnection.ts
2506
- import WebSocket2 from "ws";
2507
-
2508
- // src/tui/blessed/config.ts
2509
- var defaultTuiConfig = {
2510
- maxRequestPairs: 100,
2511
- visibleRequestCount: 10,
2512
- viewportScrollMargin: 2,
2513
- inactivityHttpSelectorTimeoutMs: 1e4
2514
- };
2515
- function getTuiConfig() {
2516
- return {
2517
- maxRequestPairs: defaultTuiConfig.maxRequestPairs,
2518
- visibleRequestCount: defaultTuiConfig.visibleRequestCount,
2519
- viewportScrollMargin: defaultTuiConfig.viewportScrollMargin,
2520
- inactivityHttpSelectorTimeoutMs: defaultTuiConfig.inactivityHttpSelectorTimeoutMs
2521
- };
2522
- }
2523
-
2524
- // src/tui/blessed/webDebuggerConnection.ts
2525
- function createWebDebuggerConnection(webDebuggerUrl, onUpdate) {
2526
- const pairs = /* @__PURE__ */ new Map();
2527
- const pairKeys = [];
2528
- let socket = null;
2529
- let reconnectTimeout = null;
2530
- let isStopped = false;
2531
- const config = getTuiConfig();
2532
- const maxPairs = config.maxRequestPairs;
2533
- const trimPairs = () => {
2534
- while (pairKeys.length > maxPairs) {
2535
- const oldestKey = pairKeys.shift();
2536
- if (oldestKey !== void 0) {
2537
- pairs.delete(oldestKey);
2538
- }
2539
- }
2540
- };
2541
- const upsertPair = (key, pair) => {
2542
- if (!pairs.has(key)) {
2543
- pairKeys.push(key);
2544
- }
2545
- pairs.set(key, pair);
2546
- trimPairs();
2547
- };
2548
- const connect = () => {
2549
- const ws = new WebSocket2(`ws://${webDebuggerUrl}/introspec/websocket`);
2550
- socket = ws;
2551
- ws.on("open", () => {
2552
- logger.info("Web debugger connected.");
2553
- });
2554
- ws.on("message", (data) => {
2555
- try {
2556
- const raw = data.toString();
2557
- const parsed = JSON.parse(raw);
2558
- const msg = {
2559
- Req: parsed.req,
2560
- Res: parsed.res
2561
- };
2562
- if (msg.Req) {
2563
- const { key } = msg.Req;
2564
- const existing = pairs.get(key);
2565
- const merged = {
2566
- request: msg.Req,
2567
- response: existing?.response
2568
- };
2569
- upsertPair(key, merged);
2570
- }
2571
- if (msg.Res) {
2572
- const { key } = msg.Res;
2573
- const existing = pairs.get(key);
2574
- const merged = {
2575
- request: existing?.request ?? {},
2576
- response: msg.Res
2577
- };
2578
- upsertPair(key, merged);
2579
- }
2580
- const reversedPairs = [];
2581
- for (let i = pairKeys.length - 1; i >= 0; i--) {
2582
- const key = pairKeys[i];
2583
- const pair = pairs.get(key);
2584
- if (pair) {
2585
- reversedPairs.push(pair);
2586
- }
2587
- }
2588
- onUpdate(reversedPairs);
2589
- } catch (err) {
2590
- logger.error("Error parsing WebSocket message:", err.message || err);
2591
- }
2592
- });
2593
- ws.on("close", () => {
2594
- logger.warn("Web debugger disconnected. Reconnecting in 5s...");
2595
- if (!isStopped) {
2596
- reconnectTimeout = setTimeout(connect, 5e3);
2597
- }
2598
- });
2599
- ws.on("error", (err) => {
2600
- logger.error(`WebSocket error: ${err.message}`);
2601
- });
2602
- };
2603
- connect();
2604
- return {
2605
- close: () => {
2606
- isStopped = true;
2607
- if (socket) {
2608
- socket.close();
2609
- }
2610
- if (reconnectTimeout) {
2611
- clearTimeout(reconnectTimeout);
2612
- }
2613
- }
2614
- };
2615
- }
2616
-
2617
- // src/tui/blessed/components/UIComponents.ts
2618
- import blessed from "blessed";
2619
-
2620
- // src/tui/ink/asciArt.ts
2621
- var asciiArtPinggyLogo = `
2622
- \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557
2623
- \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u255A\u2588\u2588\u2557 \u2588\u2588\u2554\u255D
2624
- \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2554\u255D
2625
- \u2588\u2588\u2554\u2550\u2550\u2550\u255D \u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u255A\u2588\u2588\u2554\u255D
2626
- \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551
2627
- \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D `;
2628
-
2629
- // src/tui/blessed/components/UIComponents.ts
2630
- var MIN_WIDTH_WARNING = 60;
2631
- var SIMPLE_LAYOUT_THRESHOLD = 80;
2632
- function colorizeGradient(text) {
2633
- const colors = ["red", "yellow", "green", "cyan", "blue", "magenta"];
2634
- const lines = text.split("\n");
2635
- return lines.map((line, i) => {
2636
- const color = colors[i % colors.length];
2637
- return `{${color}-fg}${line}{/${color}-fg}`;
2638
- }).join("\n");
2639
- }
2640
- function createWarningUI(screen) {
2641
- return blessed.box({
2642
- parent: screen,
2643
- top: "center",
2644
- left: "center",
2645
- width: "80%",
2646
- height: 5,
2647
- content: `{red-fg}{bold}Terminal is too narrow to show TUI (${screen.width} cols).{/bold}{/red-fg}
2648
- {yellow-fg}Please resize your terminal to at least ${MIN_WIDTH_WARNING} columns for proper display.{/yellow-fg}`,
2649
- tags: true,
2650
- align: "center",
2651
- valign: "middle",
2652
- style: {
2653
- fg: "red"
2654
- }
2655
- });
2656
- }
2657
- function createFullUI(screen, urls, greet, tunnelConfig) {
2658
- const mainContainer = blessed.box({
2659
- parent: screen,
2660
- top: 0,
2661
- left: 0,
2662
- width: "100%",
2663
- height: "100%",
2664
- padding: 1
2665
- });
2666
- const logoBox = blessed.box({
2667
- parent: mainContainer,
2668
- top: 0,
2669
- left: 0,
2670
- width: "100%",
2671
- height: 7,
2672
- content: colorizeGradient(asciiArtPinggyLogo),
2673
- tags: true
2674
- });
2675
- const contentBox = blessed.box({
2676
- parent: mainContainer,
2677
- top: 8,
2678
- left: 0,
2679
- width: "100%-2",
2680
- height: "100%-10",
2681
- padding: 0,
2682
- border: {
2683
- type: "line"
2684
- },
2685
- style: {
2686
- border: {
2687
- fg: "green"
2688
- }
2689
- }
2690
- });
2691
- let greetHeight = 0;
2692
- if (greet) {
2693
- const greetBox = blessed.box({
2694
- parent: contentBox,
2695
- top: 0,
2696
- left: "center",
2697
- width: "60%",
2698
- height: 4,
2699
- content: `{bold}${greet}{/bold}`,
2700
- tags: true,
2701
- align: "center",
2702
- style: {
2703
- fg: "green"
2704
- }
2705
- });
2706
- greetHeight = 4;
2707
- }
2708
- const upperSectionTop = greetHeight > 0 ? greetHeight : 0;
2709
- const upperSection = blessed.box({
2710
- parent: contentBox,
2711
- top: upperSectionTop,
2712
- left: 0,
2713
- width: "100%-2",
2714
- height: 10
2715
- });
2716
- const urlsBox = blessed.box({
2717
- parent: upperSection,
2718
- top: 0,
2719
- left: 0,
2720
- width: "48%",
2721
- height: "100%",
2722
- padding: { left: 1, right: 1 },
2723
- tags: true
2724
- });
2725
- const statsBox = blessed.box({
2726
- parent: upperSection,
2727
- top: 0,
2728
- right: 0,
2729
- left: "65%",
2730
- width: "35%",
2731
- height: "100%",
2732
- padding: { left: 1, right: 1 },
2733
- tags: true,
2734
- align: "left"
2735
- });
2736
- const lowerSectionTop = greetHeight + 11;
2737
- const lowerSection = blessed.box({
2738
- parent: contentBox,
2739
- top: lowerSectionTop,
2740
- left: 0,
2741
- right: 0,
2742
- bottom: 2,
2743
- width: "100%-2",
2744
- height: `100%-${lowerSectionTop + 6}`
2745
- });
2746
- const isQrCodeRequested = tunnelConfig?.qrCode || false;
2747
- const requestsBox = blessed.box({
2748
- parent: lowerSection,
2749
- top: 0,
2750
- left: 0,
2751
- width: isQrCodeRequested ? "60%" : "80%",
2752
- height: "80%",
2753
- padding: { left: 1, right: 1 },
2754
- tags: true,
2755
- scrollable: true
2756
- });
2757
- let qrCodeBox;
2758
- if (isQrCodeRequested) {
2759
- qrCodeBox = blessed.box({
2760
- parent: lowerSection,
2761
- top: 0,
2762
- right: 0,
2763
- width: "40%",
2764
- height: "100%",
2765
- tags: true,
2766
- padding: { left: 1, right: 1 }
2767
- });
2768
- }
2769
- const footerBox = blessed.box({
2770
- parent: contentBox,
2771
- bottom: 0,
2772
- left: "center",
2773
- width: "shrink",
2774
- height: 1,
2775
- content: "Press Ctrl+C to stop the tunnel. Or press h for key bindings.",
2776
- tags: true
2777
- });
2778
- return {
2779
- mainContainer,
2780
- logoBox,
2781
- contentBox,
2782
- urlsBox,
2783
- statsBox,
2784
- requestsBox,
2785
- qrCodeBox,
2786
- footerBox
2787
- };
2788
- }
2789
- function createSimpleUI(screen, urls, greet) {
2790
- const mainContainer = blessed.box({
2791
- parent: screen,
2792
- top: 0,
2793
- left: 0,
2794
- width: "100%",
2795
- height: "100%",
2796
- padding: { left: 1, right: 1 }
2797
- });
2798
- let currentTop = 0;
2799
- if (greet) {
2800
- blessed.box({
2801
- parent: mainContainer,
2802
- top: currentTop,
2803
- left: "center",
2804
- width: "90%",
2805
- height: "shrink",
2806
- content: `{bold}${greet}{/bold}`,
2807
- tags: true,
2808
- align: "center",
2809
- style: {
2810
- fg: "green"
2811
- }
2812
- });
2813
- const lines = Math.ceil(greet.length / (screen.width * 0.9));
2814
- currentTop += Math.max(lines, 1) + 1;
2815
- }
2816
- const urlsBox = blessed.box({
2817
- parent: mainContainer,
2818
- top: currentTop,
2819
- left: 0,
2820
- width: "100%",
2821
- height: urls.length + 2,
2822
- tags: true
2823
- });
2824
- currentTop += urls.length + 3;
2825
- const statsBox = blessed.box({
2826
- parent: mainContainer,
2827
- top: currentTop,
2828
- left: 0,
2829
- width: "100%",
2830
- height: 8,
2831
- tags: true
2832
- });
2833
- currentTop += 9;
2834
- const footerBox = blessed.box({
2835
- parent: mainContainer,
2836
- bottom: 0,
2837
- left: "center",
2838
- width: "shrink",
2839
- height: 1,
2840
- content: "Press Ctrl+C to stop the tunnel.",
2841
- tags: true,
2842
- style: {
2843
- fg: "white"
2844
- }
2845
- });
2846
- return {
2847
- mainContainer,
2848
- urlsBox,
2849
- statsBox,
2850
- footerBox
2851
- };
2852
- }
2853
-
2854
- // src/tui/ink/utils/utils.ts
2855
- function getStatusColor(status) {
2856
- const match = status.match(/\b(\d{3})\b/);
2857
- const statusCode = match ? parseInt(match[1], 10) : 0;
2858
- switch (true) {
2859
- case (statusCode >= 100 && statusCode < 200):
2860
- return "yellow";
2861
- case (statusCode >= 200 && statusCode < 300):
2862
- return "green";
2863
- case (statusCode >= 300 && statusCode < 400):
2864
- return "yellow";
2865
- case (statusCode >= 400 && statusCode < 500):
2866
- return "red";
2867
- case statusCode >= 500:
2868
- return "pink";
2869
- default:
2870
- return "yellow";
2871
- }
2872
- }
2873
- function getBytesInt(b) {
2874
- if (b >= 1024 * 1024 * 1024) {
2875
- return `${(b / (1024 * 1024 * 1024)).toFixed(2)} G`;
2876
- }
2877
- if (b >= 1024 * 1024) {
2878
- return `${(b / (1024 * 1024)).toFixed(2)} M`;
2879
- }
2880
- if (b >= 1024) {
2881
- return `${(b / 1024).toFixed(2)} K`;
2882
- }
2883
- return `${b.toFixed(2)} `;
2884
- }
2885
-
2886
- // src/tui/blessed/components/DisplayUpdaters.ts
2887
- function updateUrlsDisplay(urlsBox, screen, urls, currentQrIndex) {
2888
- if (!urlsBox) return;
2889
- let content = "{green-fg}{bold}Public URLs{/bold}{/green-fg}\n";
2890
- urls.forEach((url, index) => {
2891
- const isSelected = index === currentQrIndex;
2892
- const prefix = isSelected ? "\u2192 " : "\u2022 ";
2893
- const color = isSelected ? "yellow" : "magenta";
2894
- if (isSelected) {
2895
- content += `{bold}{${color}-fg}${prefix}${url}{/${color}-fg}{/bold}
2896
- `;
2897
- } else {
2898
- content += `{${color}-fg}${prefix}${url}{/${color}-fg}
2899
- `;
2900
- }
2901
- });
2902
- urlsBox.setContent(content);
2903
- screen.render();
2904
- }
2905
- function updateStatsDisplay(statsBox, screen, stats) {
2906
- if (!statsBox) return;
2907
- const content = `{green-fg}{bold}Live Stats{/bold}{/green-fg}
2908
- Elapsed: ${stats.elapsedTime}s
2909
- Live Connections: ${stats.numLiveConnections}
2910
- Total Connections: ${stats.numTotalConnections}
2911
- Request: ${getBytesInt(stats.numTotalReqBytes)}
2912
- Response: ${getBytesInt(stats.numTotalResBytes)}
2913
- Total Transfer: ${getBytesInt(stats.numTotalTxBytes)}`;
2914
- statsBox.setContent(content);
2915
- statsBox.style = { ...statsBox.style };
2916
- statsBox.parseContent();
2917
- screen.render();
2918
- }
2919
- function updateRequestsDisplay(requestsBox, screen, pairs, selectedIndex) {
2920
- const config = getTuiConfig();
2921
- const { maxRequestPairs, visibleRequestCount, viewportScrollMargin } = config;
2922
- if (!requestsBox) {
2923
- return { adjustedSelectedIndex: selectedIndex, trimmedPairs: pairs };
2924
- }
2925
- let allPairs = pairs;
2926
- let trimmedPairs = pairs;
2927
- if (allPairs.length > maxRequestPairs) {
2928
- allPairs = allPairs.slice(0, maxRequestPairs);
2929
- trimmedPairs = allPairs;
2930
- }
2931
- const totalPairs = allPairs.length;
2932
- let adjustedSelectedIndex = selectedIndex;
2933
- if (adjustedSelectedIndex >= totalPairs) {
2934
- adjustedSelectedIndex = -1;
2935
- }
2936
- let viewportStart;
2937
- if (totalPairs <= visibleRequestCount) {
2938
- viewportStart = 0;
2939
- } else if (adjustedSelectedIndex === -1) {
2940
- viewportStart = 0;
2941
- } else {
2942
- viewportStart = 0;
2943
- if (adjustedSelectedIndex >= visibleRequestCount - viewportScrollMargin) {
2944
- viewportStart = Math.min(
2945
- totalPairs - visibleRequestCount,
2946
- adjustedSelectedIndex - viewportScrollMargin
2947
- );
2948
- }
2949
- if (adjustedSelectedIndex < viewportStart + viewportScrollMargin) {
2950
- viewportStart = Math.max(0, adjustedSelectedIndex - viewportScrollMargin);
2951
- }
2952
- }
2953
- const viewportEnd = Math.min(viewportStart + visibleRequestCount, totalPairs);
2954
- const visiblePairs = allPairs.slice(viewportStart, viewportEnd);
2955
- let content = "{yellow-fg}HTTP Requests:{/yellow-fg}";
2956
- if (viewportStart > 0) {
2957
- content += ` {gray-fg}\u2191 ${viewportStart} more{/gray-fg}`;
2958
- }
2959
- content += "\n";
2960
- visiblePairs.forEach((pair, i) => {
2961
- const globalIndex = viewportStart + i;
2962
- const isSelected = adjustedSelectedIndex !== -1 && adjustedSelectedIndex === globalIndex;
2963
- const prefix = isSelected ? "> " : " ";
2964
- const method = pair.request?.method || "";
2965
- const uri = pair.request?.uri || "";
2966
- const status = pair.response?.status || "";
2967
- const statusColor = getStatusColor(String(status));
2968
- if (isSelected) {
2969
- content += `{cyan-fg}${prefix}${method} ${status} ${uri}{/cyan-fg}
2970
- `;
2971
- } else if (pair.response) {
2972
- content += `{${statusColor}-fg}${prefix}${method} ${status} ${uri}{/${statusColor}-fg}
2973
- `;
2974
- } else {
2975
- content += `${prefix}${method} ...${uri}
2976
- `;
2977
- }
2978
- });
2979
- const itemsBelow = totalPairs - viewportEnd;
2980
- if (itemsBelow > 0) {
2981
- content += `{gray-fg} \u2193 ${itemsBelow} more{/gray-fg}
2982
- `;
2983
- }
2984
- requestsBox.setContent(content);
2985
- screen.render();
2986
- return { adjustedSelectedIndex, trimmedPairs };
2987
- }
2988
- function updateQrCodeDisplay(qrCodeBox, screen, qrCodes, urls, currentQrIndex) {
2989
- if (!qrCodeBox || qrCodes.length === 0) return;
2990
- let content = `{green-fg}{bold}QR Code ${currentQrIndex + 1}/${urls.length}{/bold}{/green-fg}
2991
- `;
2992
- if (urls.length > 1) {
2993
- content += "\n{yellow-fg}\u2190 \u2192 to switch QR codes{/yellow-fg}\n";
2994
- }
2995
- content += qrCodes[currentQrIndex] || "";
2996
- qrCodeBox.setContent(content);
2997
- qrCodeBox.style = { ...qrCodeBox.style };
2998
- qrCodeBox.parseContent();
2999
- screen.render();
3000
- }
3001
-
3002
- // src/tui/blessed/components/Modals.ts
3003
- import blessed2 from "blessed";
3004
- function showDetailModal(screen, manager, requestText, responseText) {
3005
- manager.inDetailView = true;
3006
- manager.detailModal = blessed2.box({
3007
- parent: screen,
3008
- top: "center",
3009
- left: "center",
3010
- width: "90%",
3011
- height: "90%",
3012
- border: {
3013
- type: "line"
3014
- },
3015
- style: {
3016
- border: {
3017
- fg: "green"
3018
- }
3019
- },
3020
- padding: { left: 2, right: 2, top: 1, bottom: 1 },
3021
- tags: true,
3022
- scrollable: true,
3023
- keys: true,
3024
- vi: true,
3025
- alwaysScroll: true,
3026
- scrollbar: {
3027
- ch: " ",
3028
- track: {
3029
- bg: "cyan"
3030
- },
3031
- style: {
3032
- inverse: true
3033
- }
3034
- }
3035
- });
3036
- const content = `{cyan-fg}{bold}Request{/bold}{/cyan-fg}
3037
- ${requestText || "(no request data)"}
3038
-
3039
- {magenta-fg}{bold}Response{/bold}{/magenta-fg}
3040
- ${responseText || "(no response data)"}
3041
-
3042
- {white-bg}{black-fg}Press ESC to close{/black-fg}{/white-bg}`;
3043
- manager.detailModal.setContent(content);
3044
- manager.detailModal.focus();
3045
- screen.render();
3046
- }
3047
- function closeDetailModal(screen, manager) {
3048
- if (manager.detailModal) {
3049
- manager.detailModal.destroy();
3050
- manager.detailModal = null;
3051
- }
3052
- manager.inDetailView = false;
3053
- screen.render();
3054
- }
3055
- function showKeyBindingsModal(screen, manager) {
3056
- manager.keyBindingView = true;
3057
- manager.keyBindingsModal = blessed2.box({
3058
- parent: screen,
3059
- top: "center",
3060
- left: "center",
3061
- width: "60%",
3062
- height: "80%",
3063
- border: {
3064
- type: "line"
3065
- },
3066
- style: {
3067
- border: {
3068
- fg: "green"
3069
- }
3070
- },
3071
- padding: { left: 2, right: 2, top: 1, bottom: 1 },
3072
- tags: true
3073
- });
3074
- const content = `{cyan-fg}{bold}Key Bindings{/bold}{/cyan-fg}
3075
-
3076
- {bold}h{/bold} This page
3077
- {bold}c{/bold} Copy the selected URL to clipboard
3078
- {bold}Ctrl+c{/bold} Exit
3079
-
3080
- Enter/Return Open selected request
3081
- Esc Return to main page (or close modals)
3082
- UP (\u2191) Scroll up the requests
3083
- Down (\u2193) Scroll down the requests
3084
- Left (\u2190) Show qr code for previous url
3085
- Right (\u2192) Show qr code for next url
3086
- Home Jump to top of requests
3087
- End Jump to bottom of requests
3088
- Ctrl+c Force Exit
3089
-
3090
- {white-bg}{black-fg}Press ESC to close{/black-fg}{/white-bg}`;
3091
- manager.keyBindingsModal.setContent(content);
3092
- manager.keyBindingsModal.focus();
3093
- screen.render();
3094
- }
3095
- function closeKeyBindingsModal(screen, manager) {
3096
- if (manager.keyBindingsModal) {
3097
- manager.keyBindingsModal.destroy();
3098
- manager.keyBindingsModal = null;
3099
- }
3100
- manager.keyBindingView = false;
3101
- screen.render();
3102
- }
3103
- function showDisconnectModal(screen, manager, message, onClose) {
3104
- manager.inDisconnectView = true;
3105
- manager.disconnectModal = blessed2.box({
3106
- parent: screen,
3107
- top: "center",
3108
- left: "center",
3109
- width: "50%",
3110
- height: "20%",
3111
- border: {
3112
- type: "line"
3113
- },
3114
- style: {
3115
- border: {
3116
- fg: "red"
3117
- }
3118
- },
3119
- padding: { left: 2, right: 2, top: 1, bottom: 1 },
3120
- tags: true,
3121
- align: "center",
3122
- valign: "middle"
3123
- });
3124
- const content = `{red-fg}{bold}Tunnel Disconnected{/bold}{/red-fg}
3125
-
3126
- ${message || "Disconnect request received. Tunnel will be closed."}
3127
-
3128
- {white-bg}{black-fg}Closing in 3 seconds... {/black-fg}{/white-bg}`;
3129
- manager.disconnectModal.setContent(content);
3130
- manager.disconnectModal.focus();
3131
- screen.render();
3132
- const timeout = setTimeout(() => {
3133
- closeDisconnectModal(screen, manager);
3134
- if (onClose) onClose();
3135
- }, 5e3);
3136
- const keyHandler = () => {
3137
- clearTimeout(timeout);
3138
- closeDisconnectModal(screen, manager);
3139
- if (onClose) onClose();
3140
- };
3141
- manager.disconnectModal.key(["escape", "enter", "space"], keyHandler);
3142
- screen.key(["escape", "enter", "space"], keyHandler);
3143
- }
3144
- function closeDisconnectModal(screen, manager) {
3145
- if (manager.disconnectModal) {
3146
- manager.disconnectModal.destroy();
3147
- manager.disconnectModal = null;
3148
- }
3149
- manager.inDisconnectView = false;
3150
- screen.render();
3151
- }
3152
- function showLoadingModal(screen, modalManager, message = "Loading...") {
3153
- if (modalManager.loadingView) return;
3154
- modalManager.loadingBox = blessed2.box({
3155
- parent: screen,
3156
- top: "center",
3157
- left: "center",
3158
- width: "60%",
3159
- height: 8,
3160
- border: { type: "line" },
3161
- style: {
3162
- border: { fg: "yellow" }
3163
- },
3164
- tags: true,
3165
- content: `{center}{yellow-fg}{bold}${message}{/bold}{/yellow-fg}
3166
-
3167
- {gray-fg}Press ESC to cancel{/gray-fg}{/center}`,
3168
- valign: "middle"
3169
- });
3170
- modalManager.loadingView = true;
3171
- screen.render();
3172
- }
3173
- function closeLoadingModal(screen, modalManager) {
3174
- if (!modalManager.loadingView || !modalManager.loadingBox) return;
3175
- modalManager.loadingBox.destroy();
3176
- modalManager.loadingBox = null;
3177
- modalManager.loadingView = false;
3178
- screen.render();
3179
- }
3180
- function showErrorModal(screen, modalManager, title = "Error", message) {
3181
- if (modalManager.loadingBox) {
3182
- modalManager.loadingBox.destroy();
3183
- modalManager.loadingBox = null;
3184
- }
3185
- modalManager.loadingBox = blessed2.box({
3186
- parent: screen,
3187
- top: "center",
3188
- left: "center",
3189
- width: "60%",
3190
- height: 9,
3191
- border: { type: "line" },
3192
- style: {
3193
- border: { fg: "red" }
3194
- },
3195
- tags: true,
3196
- content: `{center}{red-fg}{bold}${title}{/bold}{/red-fg}
3197
-
3198
- {white-fg}${message}{/white-fg}
3199
-
3200
- {gray-fg}Press ESC to close{/gray-fg}{/center}`,
3201
- valign: "middle"
3202
- });
3203
- modalManager.loadingView = true;
3204
- screen.render();
3205
- }
3206
-
3207
- // src/tui/blessed/headerFetcher.ts
3208
- async function fetchReqResHeaders(baseUrl, key, signal) {
3209
- if (!baseUrl) {
3210
- return { req: "", res: "" };
3211
- }
3212
- try {
3213
- const [reqRes, resRes] = await Promise.all([
3214
- fetch(`http://${baseUrl}/introspec/getrawrequestheader`, {
3215
- headers: { "X-Introspec-Key": key.toString() },
3216
- signal
3217
- }),
3218
- fetch(`http://${baseUrl}/introspec/getrawresponseheader`, {
3219
- headers: { "X-Introspec-Key": key.toString() },
3220
- signal
3221
- })
3222
- ]);
3223
- const [req, res] = await Promise.all([reqRes.text(), resRes.text()]);
3224
- return { req, res };
3225
- } catch (err) {
3226
- if (err?.name === "AbortError") {
3227
- throw err;
3228
- }
3229
- logger.error("Error fetching headers:", err.message || err);
3230
- throw err;
3231
- }
3232
- }
3233
-
3234
- // src/tui/blessed/components/KeyBindings.ts
3235
- function setupKeyBindings(screen, modalManager, state, callbacks, tunnelConfig) {
3236
- let inactivityTimeout = null;
3237
- const { inactivityHttpSelectorTimeoutMs } = getTuiConfig();
3238
- const INACTIVITY_TIMEOUT_MS = inactivityHttpSelectorTimeoutMs;
3239
- const resetInactivityTimer = () => {
3240
- if (inactivityTimeout) {
3241
- clearTimeout(inactivityTimeout);
3242
- }
3243
- if (state.selectedIndex !== -1) {
3244
- inactivityTimeout = setTimeout(() => {
3245
- callbacks.onSelectedIndexChange(-1, null);
3246
- callbacks.updateRequestsDisplay();
3247
- }, INACTIVITY_TIMEOUT_MS);
3248
- }
3249
- };
3250
- screen.key(["C-c"], () => {
3251
- callbacks.onDestroy();
3252
- process.exit(0);
3253
- });
3254
- screen.key(["escape"], () => {
3255
- if (modalManager.loadingView) {
3256
- if (modalManager.fetchAbortController) {
3257
- modalManager.fetchAbortController.abort();
3258
- modalManager.fetchAbortController = null;
3259
- }
3260
- closeLoadingModal(screen, modalManager);
3261
- return;
3262
- }
3263
- if (modalManager.inDetailView) {
3264
- closeDetailModal(screen, modalManager);
3265
- return;
3266
- }
3267
- if (modalManager.keyBindingView) {
3268
- closeKeyBindingsModal(screen, modalManager);
3269
- return;
3270
- }
3271
- if (state.selectedIndex !== -1) {
3272
- if (inactivityTimeout) {
3273
- clearTimeout(inactivityTimeout);
3274
- inactivityTimeout = null;
3275
- }
3276
- callbacks.onSelectedIndexChange(-1, null);
3277
- callbacks.updateRequestsDisplay();
3278
- }
3279
- });
3280
- screen.key(["up"], () => {
3281
- if (modalManager.inDetailView || modalManager.keyBindingView || modalManager.loadingView) return;
3282
- resetInactivityTimer();
3283
- if (state.selectedIndex === -1) {
3284
- const requestKey = state.pairs[0]?.request?.key ?? null;
3285
- callbacks.onSelectedIndexChange(0, requestKey);
3286
- callbacks.updateRequestsDisplay();
3287
- resetInactivityTimer();
3288
- } else if (state.selectedIndex > 0) {
3289
- const newIndex = state.selectedIndex - 1;
3290
- const requestKey = state.pairs[newIndex]?.request?.key ?? null;
3291
- callbacks.onSelectedIndexChange(newIndex, requestKey);
3292
- callbacks.updateRequestsDisplay();
3293
- }
3294
- });
3295
- screen.key(["down"], () => {
3296
- if (modalManager.inDetailView || modalManager.keyBindingView || modalManager.loadingView) return;
3297
- resetInactivityTimer();
3298
- const config = getTuiConfig();
3299
- const limitedLength = Math.min(state.pairs.length, config.maxRequestPairs);
3300
- if (state.selectedIndex === -1) {
3301
- if (limitedLength > 0) {
3302
- const requestKey = state.pairs[0]?.request?.key ?? null;
3303
- callbacks.onSelectedIndexChange(0, requestKey);
3304
- callbacks.updateRequestsDisplay();
3305
- resetInactivityTimer();
3306
- }
3307
- } else if (state.selectedIndex < limitedLength - 1) {
3308
- const newIndex = state.selectedIndex + 1;
3309
- const requestKey = state.pairs[newIndex]?.request?.key ?? null;
3310
- callbacks.onSelectedIndexChange(newIndex, requestKey);
3311
- callbacks.updateRequestsDisplay();
3312
- }
3313
- });
3314
- screen.key(["end"], () => {
3315
- if (modalManager.inDetailView || modalManager.keyBindingView || modalManager.loadingView) return;
3316
- resetInactivityTimer();
3317
- const config = getTuiConfig();
3318
- const limitedLength = Math.min(state.pairs.length, config.maxRequestPairs);
3319
- const lastIndex = Math.max(0, limitedLength - 1);
3320
- if (state.selectedIndex !== lastIndex) {
3321
- const requestKey = state.pairs[lastIndex]?.request?.key ?? null;
3322
- callbacks.onSelectedIndexChange(lastIndex, requestKey);
3323
- callbacks.updateRequestsDisplay();
3324
- }
3325
- });
3326
- screen.key(["enter"], async () => {
3327
- if (modalManager.inDetailView || modalManager.keyBindingView || modalManager.loadingView) return;
3328
- if (state.selectedIndex === -1) return;
3329
- resetInactivityTimer();
3330
- const pair = state.pairs[state.selectedIndex];
3331
- if (pair?.request?.key !== void 0 && pair?.request?.key !== null) {
3332
- const abortController = new AbortController();
3333
- modalManager.fetchAbortController = abortController;
3334
- showLoadingModal(screen, modalManager, "Fetching request details...");
3335
- try {
3336
- const headers = await fetchReqResHeaders(
3337
- tunnelConfig?.webDebugger || "",
3338
- pair.request.key,
3339
- abortController.signal
3340
- );
3341
- if (abortController.signal.aborted) {
3342
- return;
3343
- }
3344
- closeLoadingModal(screen, modalManager);
3345
- modalManager.fetchAbortController = null;
3346
- showDetailModal(screen, modalManager, headers.req, headers.res);
3347
- } catch (err) {
3348
- if (err?.name === "AbortError" || abortController.signal.aborted) {
3349
- logger.info("Fetch request cancelled by user");
3350
- return;
3351
- }
3352
- closeLoadingModal(screen, modalManager);
3353
- modalManager.fetchAbortController = null;
3354
- const errorMessage = err?.message || String(err) || "Unknown error occurred";
3355
- logger.error("Fetch error:", err);
3356
- showErrorModal(screen, modalManager, "Failed to fetch request details", errorMessage);
3357
- }
3358
- }
3359
- });
3360
- screen.key(["h"], () => {
3361
- if (modalManager.inDetailView || modalManager.loadingView) return;
3362
- if (modalManager.keyBindingView) {
3363
- closeKeyBindingsModal(screen, modalManager);
3364
- } else {
3365
- showKeyBindingsModal(screen, modalManager);
3366
- }
3367
- });
3368
- screen.key(["c"], async () => {
3369
- if (modalManager.inDetailView || modalManager.keyBindingView || modalManager.loadingView) return;
3370
- if (state.urls.length > 0) {
3371
- try {
3372
- const clipboardy = await import("clipboardy");
3373
- clipboardy.default.writeSync(state.urls[state.currentQrIndex]);
3374
- } catch (err) {
3375
- logger.error("Failed to copy to clipboard:", err);
3376
- }
3377
- }
3378
- });
3379
- screen.key(["left"], () => {
3380
- if (modalManager.inDetailView || modalManager.keyBindingView || modalManager.loadingView) return;
3381
- if (state.currentQrIndex > 0) {
3382
- callbacks.onQrIndexChange(state.currentQrIndex - 1);
3383
- callbacks.updateUrlsDisplay();
3384
- callbacks.updateQrCodeDisplay();
3385
- }
3386
- });
3387
- screen.key(["right"], () => {
3388
- if (modalManager.inDetailView || modalManager.keyBindingView || modalManager.loadingView) return;
3389
- if (state.currentQrIndex < state.urls.length - 1) {
3390
- callbacks.onQrIndexChange(state.currentQrIndex + 1);
3391
- callbacks.updateUrlsDisplay();
3392
- callbacks.updateQrCodeDisplay();
3393
- }
3394
- });
3395
- }
3396
-
3397
- // src/tui/blessed/TunnelTui.ts
3398
- var TunnelTui = class {
3399
- constructor(props) {
3400
- // State
3401
- this.currentQrIndex = 0;
3402
- this.selectedIndex = -1;
3403
- // -1 means no selection
3404
- this.selectedRequestKey = null;
3405
- // Track selected request by key
3406
- this.qrCodes = [];
3407
- this.stats = {
3408
- elapsedTime: 0,
3409
- numLiveConnections: 0,
3410
- numTotalConnections: 0,
3411
- numTotalReqBytes: 0,
3412
- numTotalResBytes: 0,
3413
- numTotalTxBytes: 0
3414
- };
3415
- this.pairs = [];
3416
- this.webDebuggerConnection = null;
3417
- this.modalManager = {
3418
- detailModal: null,
3419
- keyBindingsModal: null,
3420
- disconnectModal: null,
3421
- inDetailView: false,
3422
- keyBindingView: false,
3423
- inDisconnectView: false,
3424
- loadingBox: null,
3425
- loadingView: false,
3426
- fetchAbortController: null
3427
- };
3428
- this.exitPromiseResolve = null;
3429
- this.urls = props.urls;
3430
- this.greet = props.greet || "";
3431
- this.tunnelConfig = props.tunnelConfig;
3432
- this.disconnectInfo = props.disconnectInfo;
3433
- if (props.tunnelInstance) {
3434
- this.tunnelInstance = props.tunnelInstance;
3435
- }
3436
- this.exitPromise = new Promise((resolve) => {
3437
- this.exitPromiseResolve = resolve;
3438
- });
3439
- this.screen = blessed3.screen({
3440
- smartCSR: true,
3441
- title: "Pinggy Tunnel",
3442
- fullUnicode: true
3443
- });
3444
- this.setupStatsListener();
3445
- this.setupWebDebugger();
3446
- this.generateQrCodes();
3447
- this.createUI();
3448
- this.setupKeyBindings();
3449
- }
3450
- setupStatsListener() {
3451
- globalThis.__PINGGY_TUNNEL_STATS__ = (newStats2) => {
3452
- this.stats = { ...newStats2 };
3453
- this.updateStatsDisplay();
3454
- };
3455
- }
3456
- clearSelection() {
3457
- this.selectedIndex = -1;
3458
- this.selectedRequestKey = null;
3459
- }
3460
- setupWebDebugger() {
3461
- if (this.tunnelConfig?.webDebugger) {
3462
- this.webDebuggerConnection = createWebDebuggerConnection(
3463
- this.tunnelConfig.webDebugger,
3464
- (pairs) => {
3465
- this.pairs = pairs;
3466
- if (this.selectedRequestKey !== null) {
3467
- const newIndex = pairs.findIndex(
3468
- (pair) => pair.request?.key === this.selectedRequestKey
3469
- );
3470
- if (newIndex !== -1) {
3471
- this.selectedIndex = newIndex;
3472
- } else {
3473
- this.clearSelection();
3474
- }
3475
- }
3476
- this.updateRequestsDisplay();
3477
- }
3478
- );
3479
- }
3480
- }
3481
- async generateQrCodes() {
3482
- if (this.tunnelConfig?.qrCode && this.urls.length > 0) {
3483
- this.qrCodes = await createQrCodes(this.urls);
3484
- this.updateQrCodeDisplay();
3485
- }
3486
- }
3487
- // Create the UI based on terminal size
3488
- createUI() {
3489
- this.buildUI();
3490
- this.screen.on("resize", () => {
3491
- this.handleResize();
3492
- });
3493
- }
3494
- buildUI() {
3495
- const width = this.screen.width;
3496
- if (width < MIN_WIDTH_WARNING) {
3497
- this.uiElements = {
3498
- mainContainer: createWarningUI(this.screen),
3499
- urlsBox: null,
3500
- statsBox: null,
3501
- requestsBox: null,
3502
- footerBox: null,
3503
- warningBox: createWarningUI(this.screen)
3504
- };
3505
- this.screen.render();
3506
- return;
3507
- }
3508
- if (width < SIMPLE_LAYOUT_THRESHOLD) {
3509
- this.uiElements = createSimpleUI(this.screen, this.urls, this.greet);
3510
- } else {
3511
- this.uiElements = createFullUI(this.screen, this.urls, this.greet, this.tunnelConfig);
3512
- }
3513
- this.refreshDisplays();
3514
- this.screen.render();
3515
- }
3516
- refreshDisplays() {
3517
- this.updateUrlsDisplay();
3518
- this.updateStatsDisplay();
3519
- this.updateRequestsDisplay();
3520
- this.updateQrCodeDisplay();
3521
- }
3522
- updateUrlsDisplay() {
3523
- updateUrlsDisplay(
3524
- this.uiElements?.urlsBox,
3525
- this.screen,
3526
- this.urls,
3527
- this.currentQrIndex
3528
- );
3529
- }
3530
- updateStatsDisplay() {
3531
- updateStatsDisplay(
3532
- this.uiElements?.statsBox,
3533
- this.screen,
3534
- this.stats
3535
- );
3536
- }
3537
- updateRequestsDisplay() {
3538
- const result = updateRequestsDisplay(
3539
- this.uiElements?.requestsBox,
3540
- this.screen,
3541
- this.pairs,
3542
- this.selectedIndex
3543
- );
3544
- if (result.adjustedSelectedIndex !== this.selectedIndex) {
3545
- if (result.adjustedSelectedIndex === -1) {
3546
- this.clearSelection();
3547
- } else {
3548
- this.selectedIndex = result.adjustedSelectedIndex;
3549
- }
3550
- }
3551
- if (result.trimmedPairs !== this.pairs) {
3552
- this.pairs = result.trimmedPairs;
3553
- }
3554
- }
3555
- updateQrCodeDisplay() {
3556
- updateQrCodeDisplay(
3557
- this.uiElements?.qrCodeBox,
3558
- this.screen,
3559
- this.qrCodes,
3560
- this.urls,
3561
- this.currentQrIndex
3562
- );
3563
- }
3564
- setupKeyBindings() {
3565
- const self = this;
3566
- const state = {
3567
- get currentQrIndex() {
3568
- return self.currentQrIndex;
3569
- },
3570
- set currentQrIndex(value) {
3571
- self.currentQrIndex = value;
3572
- },
3573
- get selectedIndex() {
3574
- return self.selectedIndex;
3575
- },
3576
- set selectedIndex(value) {
3577
- self.selectedIndex = value;
3578
- },
3579
- get pairs() {
3580
- return self.pairs;
3581
- },
3582
- get urls() {
3583
- return self.urls;
3584
- }
3585
- };
3586
- const callbacks = {
3587
- onQrIndexChange: (index) => {
3588
- self.currentQrIndex = index;
3589
- },
3590
- onSelectedIndexChange: (index, requestKey) => {
3591
- self.selectedIndex = index;
3592
- self.selectedRequestKey = requestKey;
3593
- },
3594
- onDestroy: () => self.destroy(),
3595
- updateUrlsDisplay: () => self.updateUrlsDisplay(),
3596
- updateQrCodeDisplay: () => self.updateQrCodeDisplay(),
3597
- updateRequestsDisplay: () => self.updateRequestsDisplay()
3598
- };
3599
- setupKeyBindings(
3600
- this.screen,
3601
- this.modalManager,
3602
- state,
3603
- callbacks,
3604
- this.tunnelConfig
3605
- );
3606
- }
3607
- handleResize() {
3608
- this.screen.children.forEach((child) => child.destroy());
3609
- this.buildUI();
3610
- }
3611
- updateDisconnectInfo(info) {
3612
- this.disconnectInfo = info;
3613
- if (info?.disconnected) {
3614
- const message = info.error ? `Error: ${info.error}
3615
- Tunnel will be closed.` : info.messages?.join("\n") || "Disconnect request received. Tunnel will be closed.";
3616
- showDisconnectModal(
3617
- this.screen,
3618
- this.modalManager,
3619
- message,
3620
- () => this.destroy()
3621
- );
3622
- }
3623
- }
3624
- start() {
3625
- this.screen.render();
3626
- }
3627
- waitUntilExit() {
3628
- return this.exitPromise;
3629
- }
3630
- destroy() {
3631
- if (this.tunnelInstance?.tunnelid) {
3632
- const manager = TunnelManager.getInstance();
3633
- manager.stopTunnel(this.tunnelInstance.tunnelid);
3634
- }
3635
- delete globalThis.__PINGGY_TUNNEL_STATS__;
3636
- if (this.webDebuggerConnection) {
3637
- this.webDebuggerConnection.close();
3638
- }
3639
- this.screen.destroy();
3640
- if (this.exitPromiseResolve) {
3641
- this.exitPromiseResolve();
3642
- }
3643
- }
3644
- };
3645
-
3646
- // src/cli/starCli.ts
3647
- var TunnelData = {
3648
- urls: null,
3649
- greet: null,
3650
- usage: null
3651
- };
3652
- var activeTui = null;
3653
- var disconnectState = null;
3654
- async function launchTui(finalConfig, urls, greet, tunnel) {
3655
- try {
3656
- const isTTYEnabled = process.stdin.isTTY;
3657
- if (!isTTYEnabled) {
3658
- printer_default.warn("Unable to initiate the TUI: your terminal does not support the required input mode.");
3659
- return;
3660
- }
3661
- const tui = new TunnelTui({
3662
- urls: urls ?? [],
3663
- greet: greet ?? "",
3664
- tunnelConfig: finalConfig,
3665
- disconnectInfo: null,
3666
- tunnelInstance: tunnel
3667
- });
3668
- activeTui = tui;
3669
- try {
3670
- tui.start();
3671
- await tui.waitUntilExit();
3672
- } catch (e) {
3673
- logger.warn("TUI error", e);
3674
- } finally {
3675
- activeTui = null;
3676
- }
3677
- } catch (e) {
3678
- logger.warn("Failed to (re-)initiate TUI", e);
3679
- }
3680
- }
3681
- async function startCli(finalConfig, manager) {
3682
- if (!finalConfig.NoTUI && finalConfig.webDebugger === "") {
3683
- const freePort = await getFreePort(finalConfig.webDebugger || "");
3684
- finalConfig.webDebugger = `localhost:${freePort}`;
3685
- }
3686
- try {
3687
- const manager2 = TunnelManager.getInstance();
3688
- const tunnel = await manager2.createTunnel(finalConfig);
3689
- printer_default.startSpinner("Connecting to Pinggy...");
3690
- if (!finalConfig.NoTUI) {
3691
- manager2.registerStatsListener(tunnel.tunnelid, (tunnelId, stats) => {
3692
- globalThis.__PINGGY_TUNNEL_STATS__?.(stats);
3693
- });
3694
- }
3695
- manager2.registerWorkerErrorListner(tunnel.tunnelid, (_tunnelid, error) => {
3696
- printer_default.error(`${error.message}`);
3697
- });
3698
- await manager2.startTunnel(tunnel.tunnelid);
3699
- printer_default.stopSpinnerSuccess(" Connected to Pinggy");
3700
- printer_default.success(pico3.bold("Tunnel established!"));
3701
- printer_default.print(pico3.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"));
3702
- TunnelData.urls = await manager2.getTunnelUrls(tunnel.tunnelid);
3703
- TunnelData.greet = await manager2.getTunnelGreetMessage(tunnel.tunnelid);
3704
- printer_default.info(pico3.cyanBright("Remote URLs:"));
3705
- (TunnelData.urls ?? []).forEach(
3706
- (url) => printer_default.print(" " + pico3.magentaBright(url))
3707
- );
3708
- printer_default.print(pico3.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"));
3709
- if (TunnelData.greet?.includes("not authenticated")) {
3710
- printer_default.warn(pico3.yellowBright(TunnelData.greet));
3711
- } else if (TunnelData.greet?.includes("authenticated as")) {
3712
- const emailMatch = /authenticated as (.+)/.exec(TunnelData.greet);
3713
- if (emailMatch) {
3714
- const email = emailMatch[1];
3715
- printer_default.info(pico3.cyanBright("Authenticated as: " + email));
3716
- }
3717
- }
3718
- printer_default.print(pico3.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"));
3719
- printer_default.print(pico3.gray("\nPress Ctrl+C to stop the tunnel.\n"));
3720
- manager2.registerDisconnectListener(tunnel.tunnelid, async (tunnelId, error, messages) => {
3721
- if (activeTui) {
3722
- disconnectState = {
3723
- disconnected: true,
3724
- error,
3725
- messages
3726
- };
3727
- activeTui.updateDisconnectInfo(disconnectState);
3728
- try {
3729
- await activeTui.waitUntilExit();
3730
- } catch (e) {
3731
- logger.warn("Failed to wait for TUI exit", e);
3732
- } finally {
3733
- activeTui = null;
3734
- printer_default.warn(`Error in tunnel:`);
3735
- messages?.forEach(function(m) {
3736
- printer_default.warnTxt(m);
3737
- });
3738
- if (!finalConfig.autoReconnect) {
3739
- process.exit(0);
3740
- }
3741
- }
3742
- } else {
3743
- messages?.forEach(function(m) {
3744
- printer_default.warn(m);
3745
- });
3746
- if (!finalConfig.autoReconnect) {
3747
- process.exit(0);
3748
- }
3749
- }
3750
- if (finalConfig.autoReconnect) {
3751
- printer_default.startSpinner("Reconnecting to Pinggy");
3752
- }
3753
- });
3754
- try {
3755
- await manager2.registerStartListener(tunnel.tunnelid, async (tunnelId, urls) => {
3756
- try {
3757
- printer_default.stopSpinnerSuccess("Reconnected to Pinggy");
3758
- } catch (e) {
3759
- }
3760
- printer_default.success(pico3.bold("Tunnel re-established!"));
3761
- printer_default.print(pico3.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"));
3762
- TunnelData.urls = urls;
3763
- TunnelData.greet = await manager2.getTunnelGreetMessage(tunnel.tunnelid);
3764
- printer_default.info(pico3.cyanBright("Remote URLs:"));
3765
- (TunnelData.urls ?? []).forEach(
3766
- (url) => printer_default.print(" " + pico3.magentaBright(url))
3767
- );
3768
- printer_default.print(pico3.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"));
3769
- if (TunnelData.greet?.includes("not authenticated")) {
3770
- printer_default.warn(pico3.yellowBright(TunnelData.greet));
3771
- } else if (TunnelData.greet?.includes("authenticated as")) {
3772
- const emailMatch = /authenticated as (.+)/.exec(TunnelData.greet);
3773
- if (emailMatch) {
3774
- const email = emailMatch[1];
3775
- printer_default.info(pico3.cyanBright("Authenticated as: " + email));
3776
- }
3777
- }
3778
- printer_default.print(pico3.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"));
3779
- printer_default.print(pico3.gray("\nPress Ctrl+C to stop the tunnel.\n"));
3780
- if (!finalConfig.NoTUI) {
3781
- await launchTui(finalConfig, TunnelData.urls, TunnelData.greet, tunnel);
3782
- }
3783
- });
3784
- } catch (e) {
3785
- logger.debug("Failed to register start listener", e);
3786
- }
3787
- if (!finalConfig.NoTUI) {
3788
- await launchTui(finalConfig, TunnelData.urls, TunnelData.greet, tunnel);
3789
- }
3790
- } catch (err) {
3791
- printer_default.stopSpinnerFail("Failed to connect");
3792
- printer_default.error(err.message || "Unknown error");
3793
- throw err;
3794
- }
3795
- }
3796
-
3797
- // src/index.ts
3798
- import { fileURLToPath as fileURLToPath2 } from "url";
3799
- import { argv } from "process";
3800
- import { realpathSync } from "fs";
3801
- async function main() {
3802
- try {
3803
- const { values, positionals, hasAnyArgs } = parseCliArgs(cliOptions);
3804
- configureLogger(values);
3805
- const manager = TunnelManager.getInstance();
3806
- process.on("SIGINT", () => {
3807
- logger.info("SIGINT received: stopping tunnels and exiting");
3808
- console.log("\nStopping all tunnels...");
3809
- manager.stopAllTunnels();
3810
- console.log("Tunnels stopped. Exiting.");
3811
- process.exit(0);
3812
- });
3813
- if (!hasAnyArgs || values.help) {
3814
- printHelpMessage();
3815
- return;
3816
- }
3817
- if (values.version) {
3818
- printer_default.print(`Pinggy CLI version: ${getVersion()}`);
3819
- return;
3820
- }
3821
- const parseResult = await parseRemoteManagement(values);
3822
- if (parseResult?.ok === false) {
3823
- printer_default.error(parseResult.error);
3824
- logger.error("Failed to initiate remote management:", parseResult.error);
3825
- process.exit(1);
3826
- }
3827
- logger.debug("Building final config from CLI values and positionals", { values, positionals });
3828
- const finalConfig = await buildFinalConfig(values, positionals);
3829
- logger.debug("Final configuration built", finalConfig);
3830
- await startCli(finalConfig, manager);
3831
- } catch (error) {
3832
- logger.error("Unhandled error in CLI:", error);
3833
- printer_default.error(error);
3834
- }
3835
- }
3836
- var currentFile = fileURLToPath2(import.meta.url);
3837
- var entryFile = null;
3838
- try {
3839
- entryFile = argv[1] ? realpathSync(argv[1]) : null;
3840
- } catch (e) {
3841
- entryFile = null;
3842
- }
3843
- if (entryFile && entryFile === currentFile) {
3844
- main();
3845
- }
3846
- export {
3847
- TunnelManager,
3848
- TunnelOperations,
3849
- closeRemoteManagement,
3850
- enablePackageLogging,
3851
- getRemoteManagementState,
3852
- initiateRemoteManagement
3853
- };