pinggy 0.3.4 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +1 -1
  2. package/dist/chunk-65R2GMKQ.js +2101 -0
  3. package/dist/index.cjs +1814 -1362
  4. package/dist/index.d.cts +616 -0
  5. package/dist/index.d.ts +616 -0
  6. package/dist/index.js +38 -55
  7. package/dist/{main-CZY6GID4.js → main-2QDG7PWL.js} +229 -1726
  8. package/package.json +3 -4
  9. package/.github/workflows/npm-publish-github-packages.yml +0 -34
  10. package/.github/workflows/publish-binaries.yml +0 -223
  11. package/Makefile +0 -4
  12. package/caxa_build.js +0 -24
  13. package/dist/chunk-T5ESYDJY.js +0 -121
  14. package/ent.plist +0 -14
  15. package/jest.config.js +0 -19
  16. package/src/_tests_/build_config.test.ts +0 -91
  17. package/src/cli/buildConfig.ts +0 -475
  18. package/src/cli/defaults.ts +0 -20
  19. package/src/cli/extendedOptions.ts +0 -153
  20. package/src/cli/help.ts +0 -43
  21. package/src/cli/options.ts +0 -50
  22. package/src/cli/starCli.ts +0 -229
  23. package/src/index.ts +0 -30
  24. package/src/logger.ts +0 -138
  25. package/src/main.ts +0 -87
  26. package/src/remote_management/handler.ts +0 -244
  27. package/src/remote_management/remoteManagement.ts +0 -226
  28. package/src/remote_management/remote_schema.ts +0 -176
  29. package/src/remote_management/websocket_handlers.ts +0 -180
  30. package/src/tui/blessed/TunnelTui.ts +0 -340
  31. package/src/tui/blessed/components/DisplayUpdaters.ts +0 -189
  32. package/src/tui/blessed/components/KeyBindings.ts +0 -236
  33. package/src/tui/blessed/components/Modals.ts +0 -302
  34. package/src/tui/blessed/components/UIComponents.ts +0 -306
  35. package/src/tui/blessed/components/index.ts +0 -4
  36. package/src/tui/blessed/config.ts +0 -53
  37. package/src/tui/blessed/headerFetcher.ts +0 -42
  38. package/src/tui/blessed/index.ts +0 -2
  39. package/src/tui/blessed/qrCodeGenerator.ts +0 -20
  40. package/src/tui/blessed/webDebuggerConnection.ts +0 -128
  41. package/src/tui/ink/asciArt.ts +0 -7
  42. package/src/tui/ink/hooks/useQrCodes.ts +0 -27
  43. package/src/tui/ink/hooks/useReqResHeaders.ts +0 -27
  44. package/src/tui/ink/hooks/useTerminalSize.ts +0 -26
  45. package/src/tui/ink/hooks/useTerminalStats.ts +0 -24
  46. package/src/tui/ink/hooks/useWebDebugger.ts +0 -98
  47. package/src/tui/ink/index.tsx +0 -243
  48. package/src/tui/ink/layout/Borders.tsx +0 -15
  49. package/src/tui/ink/layout/Container.tsx +0 -15
  50. package/src/tui/ink/sections/DebuggerDetailModal.tsx +0 -53
  51. package/src/tui/ink/sections/KeyBindings.tsx +0 -58
  52. package/src/tui/ink/sections/QrCodeSection.tsx +0 -28
  53. package/src/tui/ink/sections/StatsSection.tsx +0 -20
  54. package/src/tui/ink/sections/URLsSection.tsx +0 -53
  55. package/src/tui/ink/utils/utils.ts +0 -35
  56. package/src/tui/spinner/spinner.ts +0 -64
  57. package/src/tunnel_manager/TunnelManager.ts +0 -1212
  58. package/src/types.ts +0 -255
  59. package/src/utils/FileServer.ts +0 -112
  60. package/src/utils/detect_vc_redist_on_windows.ts +0 -167
  61. package/src/utils/getFreePort.ts +0 -41
  62. package/src/utils/htmlTemplates.ts +0 -146
  63. package/src/utils/parseArgs.ts +0 -79
  64. package/src/utils/printer.ts +0 -81
  65. package/src/utils/util.ts +0 -18
  66. package/src/workers/file_serve_worker.ts +0 -33
  67. package/tsconfig.json +0 -17
  68. package/tsup.config.ts +0 -12
package/dist/index.cjs CHANGED
@@ -29,6 +29,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
29
29
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
30
30
  mod
31
31
  ));
32
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
33
 
33
34
  // node_modules/tsup/assets/cjs_shims.js
34
35
  var getImportMetaUrl, importMetaUrl;
@@ -321,6 +322,10 @@ var init_TunnelManager = __esm({
321
322
  this.tunnelDisconnectListeners = /* @__PURE__ */ new Map();
322
323
  this.tunnelWorkerErrorListeners = /* @__PURE__ */ new Map();
323
324
  this.tunnelStartListeners = /* @__PURE__ */ new Map();
325
+ this.tunnelWillReconnectListeners = /* @__PURE__ */ new Map();
326
+ this.tunnelReconnectingListeners = /* @__PURE__ */ new Map();
327
+ this.tunnelReconnectionCompletedListeners = /* @__PURE__ */ new Map();
328
+ this.tunnelReconnectionFailedListeners = /* @__PURE__ */ new Map();
324
329
  }
325
330
  static getInstance() {
326
331
  if (!_TunnelManager.instance) {
@@ -403,6 +408,10 @@ var init_TunnelManager = __esm({
403
408
  this.setupStatsCallback(params.tunnelid, managed);
404
409
  this.setupErrorCallback(params.tunnelid, managed);
405
410
  this.setupDisconnectCallback(params.tunnelid, managed);
411
+ this.setupWillReconnectCallback(params.tunnelid, managed);
412
+ this.setupReconnectingCallback(params.tunnelid, managed);
413
+ this.setupReconnectionCompletedCallback(params.tunnelid, managed);
414
+ this.setupReconnectionFailedCallback(params.tunnelid, managed);
406
415
  this.setUpTunnelWorkerErrorCallback(params.tunnelid, managed);
407
416
  this.tunnelsByTunnelId.set(params.tunnelid, managed);
408
417
  this.tunnelsByConfigId.set(params.configid, managed);
@@ -505,6 +514,10 @@ var init_TunnelManager = __esm({
505
514
  this.tunnelDisconnectListeners.delete(tunnelId);
506
515
  this.tunnelWorkerErrorListeners.delete(tunnelId);
507
516
  this.tunnelStartListeners.delete(tunnelId);
517
+ this.tunnelWillReconnectListeners.delete(tunnelId);
518
+ this.tunnelReconnectingListeners.delete(tunnelId);
519
+ this.tunnelReconnectionCompletedListeners.delete(tunnelId);
520
+ this.tunnelReconnectionFailedListeners.delete(tunnelId);
508
521
  managed.serveWorker = null;
509
522
  managed.warnings = managed.warnings ?? [];
510
523
  managed.isStopped = true;
@@ -638,6 +651,10 @@ var init_TunnelManager = __esm({
638
651
  this.tunnelDisconnectListeners.delete(managed.tunnelid);
639
652
  this.tunnelWorkerErrorListeners.delete(managed.tunnelid);
640
653
  this.tunnelStartListeners.delete(managed.tunnelid);
654
+ this.tunnelWillReconnectListeners.delete(managed.tunnelid);
655
+ this.tunnelReconnectingListeners.delete(managed.tunnelid);
656
+ this.tunnelReconnectionCompletedListeners.delete(managed.tunnelid);
657
+ this.tunnelReconnectionFailedListeners.delete(managed.tunnelid);
641
658
  this.tunnelsByTunnelId.delete(managed.tunnelid);
642
659
  this.tunnelsByConfigId.delete(managed.configid);
643
660
  } catch (e) {
@@ -718,6 +735,10 @@ var init_TunnelManager = __esm({
718
735
  this.tunnelDisconnectListeners.delete(tunnelid);
719
736
  this.tunnelWorkerErrorListeners.delete(tunnelid);
720
737
  this.tunnelStartListeners.delete(tunnelid);
738
+ this.tunnelWillReconnectListeners.delete(tunnelid);
739
+ this.tunnelReconnectingListeners.delete(tunnelid);
740
+ this.tunnelReconnectionCompletedListeners.delete(tunnelid);
741
+ this.tunnelReconnectionFailedListeners.delete(tunnelid);
721
742
  const newTunnel = await this._createTunnelWithProcessedConfig({
722
743
  configid: currentConfigId,
723
744
  tunnelid,
@@ -974,6 +995,58 @@ var init_TunnelManager = __esm({
974
995
  logger.info("Start listener registered for tunnel", { tunnelId, listenerId });
975
996
  return listenerId;
976
997
  }
998
+ async registerWillReconnectListener(tunnelId, listener) {
999
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
1000
+ if (!managed) {
1001
+ throw new Error(`Tunnel "${tunnelId}" not found`);
1002
+ }
1003
+ if (!this.tunnelWillReconnectListeners.has(tunnelId)) {
1004
+ this.tunnelWillReconnectListeners.set(tunnelId, /* @__PURE__ */ new Map());
1005
+ }
1006
+ const listenerId = getRandomId();
1007
+ this.tunnelWillReconnectListeners.get(tunnelId).set(listenerId, listener);
1008
+ logger.info("WillReconnect listener registered for tunnel", { tunnelId, listenerId });
1009
+ return listenerId;
1010
+ }
1011
+ async registerReconnectingListener(tunnelId, listener) {
1012
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
1013
+ if (!managed) {
1014
+ throw new Error(`Tunnel "${tunnelId}" not found`);
1015
+ }
1016
+ if (!this.tunnelReconnectingListeners.has(tunnelId)) {
1017
+ this.tunnelReconnectingListeners.set(tunnelId, /* @__PURE__ */ new Map());
1018
+ }
1019
+ const listenerId = getRandomId();
1020
+ this.tunnelReconnectingListeners.get(tunnelId).set(listenerId, listener);
1021
+ logger.info("Reconnecting listener registered for tunnel", { tunnelId, listenerId });
1022
+ return listenerId;
1023
+ }
1024
+ async registerReconnectionCompletedListener(tunnelId, listener) {
1025
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
1026
+ if (!managed) {
1027
+ throw new Error(`Tunnel "${tunnelId}" not found`);
1028
+ }
1029
+ if (!this.tunnelReconnectionCompletedListeners.has(tunnelId)) {
1030
+ this.tunnelReconnectionCompletedListeners.set(tunnelId, /* @__PURE__ */ new Map());
1031
+ }
1032
+ const listenerId = getRandomId();
1033
+ this.tunnelReconnectionCompletedListeners.get(tunnelId).set(listenerId, listener);
1034
+ logger.info("ReconnectionCompleted listener registered for tunnel", { tunnelId, listenerId });
1035
+ return listenerId;
1036
+ }
1037
+ async registerReconnectionFailedListener(tunnelId, listener) {
1038
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
1039
+ if (!managed) {
1040
+ throw new Error(`Tunnel "${tunnelId}" not found`);
1041
+ }
1042
+ if (!this.tunnelReconnectionFailedListeners.has(tunnelId)) {
1043
+ this.tunnelReconnectionFailedListeners.set(tunnelId, /* @__PURE__ */ new Map());
1044
+ }
1045
+ const listenerId = getRandomId();
1046
+ this.tunnelReconnectionFailedListeners.get(tunnelId).set(listenerId, listener);
1047
+ logger.info("ReconnectionFailed listener registered for tunnel", { tunnelId, listenerId });
1048
+ return listenerId;
1049
+ }
977
1050
  /**
978
1051
  * Removes a previously registered stats listener.
979
1052
  *
@@ -1028,6 +1101,72 @@ var init_TunnelManager = __esm({
1028
1101
  logger.warn("Attempted to deregister non-existent disconnect listener", { tunnelId, listenerId });
1029
1102
  }
1030
1103
  }
1104
+ deregisterWillReconnectListener(tunnelId, listenerId) {
1105
+ const listeners = this.tunnelWillReconnectListeners.get(tunnelId);
1106
+ if (!listeners) {
1107
+ logger.warn("No will-reconnect listeners found for tunnel", { tunnelId });
1108
+ return;
1109
+ }
1110
+ ;
1111
+ const removed = listeners.delete(listenerId);
1112
+ if (removed) {
1113
+ logger.info("WillReconnect listener deregistered", { tunnelId, listenerId });
1114
+ if (listeners.size === 0) {
1115
+ this.tunnelWillReconnectListeners.delete(tunnelId);
1116
+ }
1117
+ } else {
1118
+ logger.warn("Attempted to deregister non-existent will-reconnect listener", { tunnelId, listenerId });
1119
+ }
1120
+ }
1121
+ deregisterReconnectingListener(tunnelId, listenerId) {
1122
+ const listeners = this.tunnelReconnectingListeners.get(tunnelId);
1123
+ if (!listeners) {
1124
+ logger.warn("No reconnecting listeners found for tunnel", { tunnelId });
1125
+ return;
1126
+ }
1127
+ ;
1128
+ const removed = listeners.delete(listenerId);
1129
+ if (removed) {
1130
+ logger.info("Reconnecting listener deregistered", { tunnelId, listenerId });
1131
+ if (listeners.size === 0) {
1132
+ this.tunnelReconnectingListeners.delete(tunnelId);
1133
+ }
1134
+ } else {
1135
+ logger.warn("Attempted to deregister non-existent reconnecting listener", { tunnelId, listenerId });
1136
+ }
1137
+ }
1138
+ deregisterReconnectionCompletedListener(tunnelId, listenerId) {
1139
+ const listeners = this.tunnelReconnectionCompletedListeners.get(tunnelId);
1140
+ if (!listeners) {
1141
+ logger.warn("No reconnection completed listeners found for tunnel", { tunnelId });
1142
+ return;
1143
+ }
1144
+ const removed = listeners.delete(listenerId);
1145
+ if (removed) {
1146
+ logger.info("Reconnection completed listener deregistered", { tunnelId, listenerId });
1147
+ if (listeners.size === 0) {
1148
+ this.tunnelReconnectionCompletedListeners.delete(tunnelId);
1149
+ }
1150
+ } else {
1151
+ logger.warn("Attempted to deregister non-existent reconnection completed listener", { tunnelId, listenerId });
1152
+ }
1153
+ }
1154
+ deregisterReconnectionFailedListener(tunnelId, listenerId) {
1155
+ const listeners = this.tunnelReconnectionFailedListeners.get(tunnelId);
1156
+ if (!listeners) {
1157
+ logger.warn("No reconnection failed listeners found for tunnel", { tunnelId });
1158
+ return;
1159
+ }
1160
+ const removed = listeners.delete(listenerId);
1161
+ if (removed) {
1162
+ logger.info("Reconnection failed listener deregistered", { tunnelId, listenerId });
1163
+ if (listeners.size === 0) {
1164
+ this.tunnelReconnectionFailedListeners.delete(tunnelId);
1165
+ }
1166
+ } else {
1167
+ logger.warn("Attempted to deregister non-existent reconnection failed listener", { tunnelId, listenerId });
1168
+ }
1169
+ }
1031
1170
  async getLocalserverTlsInfo(tunnelId) {
1032
1171
  const managed = this.tunnelsByTunnelId.get(tunnelId);
1033
1172
  if (!managed) {
@@ -1106,17 +1245,6 @@ var init_TunnelManager = __esm({
1106
1245
  managedTunnel.isStopped = true;
1107
1246
  managedTunnel.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
1108
1247
  }
1109
- if (managedTunnel && managedTunnel.autoReconnect) {
1110
- logger.info("Auto-reconnecting tunnel", { tunnelId });
1111
- setTimeout(async () => {
1112
- try {
1113
- await this.restartTunnel(tunnelId);
1114
- logger.info("Tunnel auto-reconnected successfully", { tunnelId });
1115
- } catch (e) {
1116
- logger.error("Failed to auto-reconnect tunnel", { tunnelId, e });
1117
- }
1118
- }, 1e4);
1119
- }
1120
1248
  const listeners = this.tunnelDisconnectListeners.get(tunnelId);
1121
1249
  if (!listeners) return;
1122
1250
  for (const [id, listener] of listeners) {
@@ -1136,6 +1264,140 @@ var init_TunnelManager = __esm({
1136
1264
  logger.warn("Failed to set up disconnect callback", { tunnelId, error });
1137
1265
  }
1138
1266
  }
1267
+ /**
1268
+ * Called when the tunnel disconnects and the SDK is about to start reconnecting.
1269
+ * Notifies registered will-reconnect listeners.
1270
+ */
1271
+ setupWillReconnectCallback(tunnelId, managed) {
1272
+ try {
1273
+ const callback = ({ error, messages }) => {
1274
+ try {
1275
+ logger.info("Tunnel will reconnect", { tunnelId, error, messages });
1276
+ const listeners = this.tunnelWillReconnectListeners.get(tunnelId);
1277
+ if (!listeners) return;
1278
+ for (const [id, listener] of listeners) {
1279
+ try {
1280
+ listener(tunnelId, error, messages);
1281
+ } catch (err) {
1282
+ logger.debug("Error in will-reconnect-listener callback", { listenerId: id, tunnelId, err });
1283
+ }
1284
+ }
1285
+ } catch (e) {
1286
+ logger.warn("Error handling will-reconnect callback", { tunnelId, e });
1287
+ }
1288
+ };
1289
+ managed.instance.setWillReconnectCallback(callback);
1290
+ logger.debug("WillReconnect callback set up for tunnel", { tunnelId });
1291
+ } catch (error) {
1292
+ logger.warn("Failed to set up will-reconnect callback", { tunnelId, error });
1293
+ }
1294
+ }
1295
+ /**
1296
+ * Called for each reconnection attempt with the current retry count.
1297
+ * Notifies registered reconnecting listeners.
1298
+ */
1299
+ setupReconnectingCallback(tunnelId, managed) {
1300
+ try {
1301
+ const callback = ({ retryCnt }) => {
1302
+ try {
1303
+ logger.info("Tunnel reconnecting", { tunnelId, retryCnt });
1304
+ const listeners = this.tunnelReconnectingListeners.get(tunnelId);
1305
+ if (!listeners) return;
1306
+ for (const [id, listener] of listeners) {
1307
+ try {
1308
+ listener(tunnelId, retryCnt);
1309
+ } catch (err) {
1310
+ logger.debug("Error in reconnecting-listener callback", { listenerId: id, tunnelId, err });
1311
+ }
1312
+ }
1313
+ } catch (e) {
1314
+ logger.warn("Error handling reconnecting callback", { tunnelId, e });
1315
+ }
1316
+ };
1317
+ managed.instance.setReconnectingCallback(callback);
1318
+ logger.debug("Reconnecting callback set up for tunnel", { tunnelId });
1319
+ } catch (error) {
1320
+ logger.warn("Failed to set up reconnecting callback", { tunnelId, error });
1321
+ }
1322
+ }
1323
+ /**
1324
+ * Called when reconnection succeeds. Updates tunnel state back to active,
1325
+ * and notifies registered reconnection-completed and start listeners with new URLs.
1326
+ */
1327
+ setupReconnectionCompletedCallback(tunnelId, managed) {
1328
+ try {
1329
+ const callback = ({ urls }) => {
1330
+ try {
1331
+ logger.info("Tunnel reconnection completed", { tunnelId, urls });
1332
+ const managedTunnel = this.tunnelsByTunnelId.get(tunnelId);
1333
+ if (managedTunnel) {
1334
+ managedTunnel.isStopped = false;
1335
+ managedTunnel.startedAt = (/* @__PURE__ */ new Date()).toISOString();
1336
+ managedTunnel.stoppedAt = null;
1337
+ }
1338
+ const listeners = this.tunnelReconnectionCompletedListeners.get(tunnelId);
1339
+ if (listeners) {
1340
+ for (const [id, listener] of listeners) {
1341
+ try {
1342
+ listener(tunnelId, urls);
1343
+ } catch (err) {
1344
+ logger.debug("Error in reconnection-completed-listener callback", { listenerId: id, tunnelId, err });
1345
+ }
1346
+ }
1347
+ }
1348
+ const startListeners = this.tunnelStartListeners.get(tunnelId);
1349
+ if (startListeners) {
1350
+ for (const [id, listener] of startListeners) {
1351
+ try {
1352
+ listener(tunnelId, urls);
1353
+ } catch (err) {
1354
+ logger.debug("Error in start-listener callback on reconnection", { listenerId: id, tunnelId, err });
1355
+ }
1356
+ }
1357
+ }
1358
+ } catch (e) {
1359
+ logger.warn("Error handling reconnection-completed callback", { tunnelId, e });
1360
+ }
1361
+ };
1362
+ managed.instance.setReconnectionCompletedCallback(callback);
1363
+ logger.debug("ReconnectionCompleted callback set up for tunnel", { tunnelId });
1364
+ } catch (error) {
1365
+ logger.warn("Failed to set up reconnection-completed callback", { tunnelId, error });
1366
+ }
1367
+ }
1368
+ /**
1369
+ * Called when all reconnection attempts are exhausted.
1370
+ * Marks the tunnel as stopped and notifies registered reconnection-failed listeners.
1371
+ */
1372
+ setupReconnectionFailedCallback(tunnelId, managed) {
1373
+ try {
1374
+ const callback = ({ retryCnt }) => {
1375
+ try {
1376
+ logger.error("Tunnel reconnection failed", { tunnelId, retryCnt });
1377
+ const managedTunnel = this.tunnelsByTunnelId.get(tunnelId);
1378
+ if (managedTunnel) {
1379
+ managedTunnel.isStopped = true;
1380
+ managedTunnel.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
1381
+ }
1382
+ const listeners = this.tunnelReconnectionFailedListeners.get(tunnelId);
1383
+ if (!listeners) return;
1384
+ for (const [id, listener] of listeners) {
1385
+ try {
1386
+ listener(tunnelId, retryCnt);
1387
+ } catch (err) {
1388
+ logger.debug("Error in reconnection-failed-listener callback", { listenerId: id, tunnelId, err });
1389
+ }
1390
+ }
1391
+ } catch (e) {
1392
+ logger.warn("Error handling reconnection-failed callback", { tunnelId, e });
1393
+ }
1394
+ };
1395
+ managed.instance.setReconnectionFailedCallback(callback);
1396
+ logger.debug("ReconnectionFailed callback set up for tunnel", { tunnelId });
1397
+ } catch (error) {
1398
+ logger.warn("Failed to set up reconnection-failed callback", { tunnelId, error });
1399
+ }
1400
+ }
1139
1401
  setUpTunnelWorkerErrorCallback(tunnelId, managed) {
1140
1402
  try {
1141
1403
  const callback = (error) => {
@@ -1251,1397 +1513,1461 @@ var init_TunnelManager = __esm({
1251
1513
  }
1252
1514
  });
1253
1515
 
1254
- // src/cli/options.ts
1255
- var cliOptions;
1256
- var init_options = __esm({
1257
- "src/cli/options.ts"() {
1258
- "use strict";
1259
- init_cjs_shims();
1260
- cliOptions = {
1261
- // SSH-like options
1262
- R: { type: "string", multiple: true, description: "Local port. Eg. -R0:localhost:3000 will forward tunnel connections to local port 3000." },
1263
- L: { type: "string", multiple: true, description: "Web Debugger address. Eg. -L4300:localhost:4300 will start web debugger on port 4300." },
1264
- o: { type: "string", multiple: true, description: "Options", hidden: true },
1265
- "server-port": { type: "string", short: "p", description: "Pinggy server port. Default: 443" },
1266
- v4: { type: "boolean", short: "4", description: "IPv4 only", hidden: true },
1267
- v6: { type: "boolean", short: "6", description: "IPv6 only", hidden: true },
1268
- // These options appear in the ssh command, but we ignore it in CLI
1269
- t: { type: "boolean", description: "hidden", hidden: true },
1270
- T: { type: "boolean", description: "hidden", hidden: true },
1271
- n: { type: "boolean", description: "hidden", hidden: true },
1272
- N: { type: "boolean", description: "hidden", hidden: true },
1273
- // Better options
1274
- type: { type: "string", description: "Type of the connection. Eg. --type tcp" },
1275
- localport: { type: "string", short: "l", description: "Takes input as [protocol:][host:]port. Eg. --localport https://localhost:8000 OR -l 3000" },
1276
- debugger: { type: "string", short: "d", description: "Port for web debugger. Eg. --debugger 4300 OR -d 4300" },
1277
- token: { type: "string", description: "Token for authentication. Eg. --token TOKEN_VALUE" },
1278
- // Logging options (CLI overrides env)
1279
- loglevel: { type: "string", description: "Logging level: ERROR, INFO, DEBUG. Overrides PINGGY_LOG_LEVEL environment variable" },
1280
- logfile: { type: "string", description: "Path to log file. Overrides PINGGY_LOG_FILE environment variable" },
1281
- v: { type: "boolean", description: "Print logs to stdout for Cli. Overrides PINGGY_LOG_STDOUT environment variable" },
1282
- vv: { type: "boolean", description: "Enable detailed logging for the Node.js SDK and Libpinggy, including both info and debug level logs." },
1283
- vvv: { type: "boolean", description: "Enable all logs from Cli, SDK and internal components." },
1284
- autoreconnect: { type: "boolean", short: "a", description: "Automatically reconnect tunnel on failure." },
1285
- // Save and load config
1286
- saveconf: { type: "string", description: "Create the configuration file based on the options provided here" },
1287
- conf: { type: "string", description: "Use the configuration file as base. Other options will be used to override this file" },
1288
- // File server
1289
- serve: { type: "string", description: "Start a webserver to serve files from the specified path. Eg --serve /path/to/files" },
1290
- // Remote Control
1291
- "remote-management": { type: "string", description: "Enable remote management of tunnels with token. Eg. --remote-management API_KEY" },
1292
- manage: { type: "string", description: "Provide a server address to manage tunnels. Eg --manage dashboard.pinggy.io" },
1293
- notui: { type: "boolean", description: "Disable TUI in remote management mode" },
1294
- // Misc
1295
- version: { type: "boolean", description: "Print version" },
1296
- // Help
1297
- help: { type: "boolean", short: "h", description: "Show this help message" }
1298
- };
1299
- }
1300
- });
1301
-
1302
- // src/cli/help.ts
1303
- function printHelpMessage() {
1304
- console.log("\nPinggy CLI Tool - Create secure tunnels to your localhost.");
1305
- console.log("\nUsage:");
1306
- console.log(" pinggy [options] -l <port>\n");
1307
- console.log("Options:");
1308
- for (const [key, value] of Object.entries(cliOptions)) {
1309
- if (value.hidden) continue;
1310
- const short = "short" in value && value.short ? `-${value.short}, ` : " ";
1311
- const optType = value.type === "boolean" ? "" : "<value>";
1312
- console.log(` ${short}--${key.padEnd(17)} ${optType.padEnd(8)} ${value.description}`);
1313
- }
1314
- console.log("\nExtended options :");
1315
- console.log(" x:https Enforce HTTPS only (redirect HTTP to HTTPS)");
1316
- console.log(" x:noreverseproxy Disable built-in reverse-proxy header injection");
1317
- console.log(" x:localservertls:host Connect to local HTTPS server with SNI");
1318
- console.log(" x:passpreflight Pass CORS preflight requests unchanged");
1319
- console.log(" a:Key:Val Add header");
1320
- console.log(" u:Key:Val Update header");
1321
- console.log(" r:Key Remove header");
1322
- console.log(" b:user:pass Basic auth");
1323
- console.log(" k:BEARER Bearer token");
1324
- console.log(" w:192.168.1.0/24 IP whitelist (CIDR)");
1325
- console.log("\nExamples (User-friendly):");
1326
- console.log(" pinggy -l 3000 # HTTP(S) tunnel to localhost port 3000");
1327
- console.log(" pinggy --type tcp -l 22 # TCP tunnel for SSH (port 22)");
1328
- console.log(" pinggy -l 8080 -d 4300 # HTTP tunnel to port 8080 with debugger running at localhost:4300");
1329
- console.log(" pinggy --token mytoken -l 3000 # Authenticated tunnel");
1330
- console.log(" pinggy x:https x:xff -l https://localhost:8443 # HTTPS-only + XFF");
1331
- console.log(" pinggy w:192.168.1.0/24 -l 8080 # IP whitelist restriction");
1332
- console.log("\nExamples (SSH-style):");
1333
- console.log(" pinggy -R0:localhost:3000 # Basic HTTP tunnel");
1334
- console.log(" pinggy --type tcp -R0:localhost:22 # TCP tunnel for SSH");
1335
- console.log(" pinggy -R0:localhost:8080 -L4300:localhost:4300 # HTTP tunnel with debugger");
1336
- console.log(" pinggy tcp@ap.example.com -R0:localhost:22 # TCP tunnel to region\n");
1516
+ // src/types.ts
1517
+ function isErrorResponse(obj) {
1518
+ return typeof obj === "object" && obj !== null && "code" in obj && "message" in obj && typeof obj.message === "string" && Object.values(ErrorCode).includes(obj.code);
1337
1519
  }
1338
- var init_help = __esm({
1339
- "src/cli/help.ts"() {
1340
- "use strict";
1341
- init_cjs_shims();
1342
- init_options();
1343
- }
1344
- });
1345
-
1346
- // src/cli/defaults.ts
1347
- var defaultOptions;
1348
- var init_defaults = __esm({
1349
- "src/cli/defaults.ts"() {
1350
- "use strict";
1351
- init_cjs_shims();
1352
- defaultOptions = {
1353
- token: void 0,
1354
- // No default token
1355
- serverAddress: "a.pinggy.io",
1356
- forwarding: "localhost:8000",
1357
- webDebugger: "",
1358
- ipWhitelist: [],
1359
- basicAuth: [],
1360
- bearerTokenAuth: [],
1361
- headerModification: [],
1362
- force: false,
1363
- xForwardedFor: false,
1364
- httpsOnly: false,
1365
- originalRequestUrl: false,
1366
- allowPreflight: false,
1367
- reverseProxy: false,
1368
- autoReconnect: false
1369
- };
1370
- }
1371
- });
1372
-
1373
- // src/cli/extendedOptions.ts
1374
- function parseExtendedOptions(options, config) {
1375
- if (!options) return;
1376
- for (const opt of options) {
1377
- const [key, value] = opt.replace(/^"|"$/g, "").split(/:(.+)/).filter(Boolean);
1378
- switch (key) {
1379
- case "x":
1380
- switch (value) {
1381
- case "https":
1382
- case "httpsonly":
1383
- config.httpsOnly = true;
1384
- break;
1385
- case "passpreflight":
1386
- case "allowpreflight":
1387
- config.allowPreflight = true;
1388
- break;
1389
- case "reverseproxy":
1390
- config.reverseProxy = false;
1391
- break;
1392
- case "xff":
1393
- config.xForwardedFor = true;
1394
- break;
1395
- case "fullurl":
1396
- case "fullrequesturl":
1397
- config.originalRequestUrl = true;
1398
- break;
1399
- default:
1400
- printer_default.warn(`Unknown extended option "${key}"`);
1401
- logger.warn(`Warning: Unknown extended option "${key}"`);
1402
- break;
1403
- }
1404
- break;
1405
- case "w":
1406
- if (value) {
1407
- const ips = value.split(",").map((ip) => ip.trim()).filter(Boolean);
1408
- const invalidIps = ips.filter((ip) => !(isValidIpV4Cidr(ip) || isValidIpV6Cidr(ip)));
1409
- if (invalidIps.length > 0) {
1410
- printer_default.warn(`Invalid IP/CIDR(s) in whitelist: ${invalidIps.join(", ")}`);
1411
- logger.warn(`Warning: Invalid IP/CIDR(s) in whitelist: ${invalidIps.join(", ")}`);
1412
- }
1413
- if (!(invalidIps.length > 0)) {
1414
- config.ipWhitelist = ips;
1415
- }
1416
- } else {
1417
- printer_default.warn(`Extended option "${opt}" for 'w' requires IP(s)`);
1418
- logger.warn(`Warning: Extended option "${opt}" for 'w' requires IP(s)`);
1419
- }
1420
- break;
1421
- case "k":
1422
- if (!config.bearerTokenAuth) config.bearerTokenAuth = [];
1423
- if (value) {
1424
- config.bearerTokenAuth.push(value);
1425
- } else {
1426
- printer_default.warn(`Extended option "${opt}" for 'k' requires a value`);
1427
- logger.warn(`Warning: Extended option "${opt}" for 'k' requires a value`);
1428
- }
1429
- break;
1430
- case "b":
1431
- if (value && value.includes(":")) {
1432
- const [username, password] = value.split(/:(.+)/);
1433
- if (!config.basicAuth) config.basicAuth = [];
1434
- config.basicAuth.push({ username, password });
1435
- } else {
1436
- printer_default.warn(`Extended option "${opt}" for 'b' requires value in format username:password`);
1437
- logger.warn(`Warning: Extended option "${opt}" for 'b' requires value in format username:password`);
1438
- }
1439
- break;
1440
- case "a":
1441
- if (value && value.includes(":")) {
1442
- const [key2, val] = value.split(/:(.+)/);
1443
- if (!config.headerModification) config.headerModification = [];
1444
- config.headerModification.push({ type: "add", key: key2, value: [val] });
1445
- } else {
1446
- printer_default.warn(`Extended option "${opt}" for 'a' requires key:value`);
1447
- logger.warn(`Warning: Extended option "${opt}" for 'a' requires key:value`);
1448
- }
1449
- break;
1450
- case "u":
1451
- if (value && value.includes(":")) {
1452
- const [key2, val] = value.split(/:(.+)/);
1453
- if (!config.headerModification) config.headerModification = [];
1454
- config.headerModification.push({ type: "update", key: key2, value: [val] });
1455
- } else {
1456
- printer_default.warn(`Extended option "${opt}" for 'u' requires key:value`);
1457
- logger.warn(`Warning: Extended option "${opt}" for 'u' requires key:value`);
1458
- }
1459
- break;
1460
- case "r":
1461
- if (value) {
1462
- if (!config.headerModification) config.headerModification = [];
1463
- config.headerModification.push({ type: "remove", key: value });
1464
- } else {
1465
- printer_default.warn(`Extended option "${opt}" for 'r' requires a key`);
1466
- }
1467
- break;
1468
- default:
1469
- printer_default.warn(`Unknown extended option "${key}"`);
1470
- break;
1471
- }
1520
+ function newErrorResponse(codeOrError, message) {
1521
+ if (typeof codeOrError === "object") {
1522
+ return codeOrError;
1472
1523
  }
1524
+ return {
1525
+ code: codeOrError,
1526
+ message
1527
+ };
1473
1528
  }
1474
- function isValidIpV4Cidr(input) {
1475
- if (input.includes("/")) {
1476
- const [ip, mask] = input.split("/");
1477
- if (!ip || !mask) return false;
1478
- const isIp4 = (0, import_net.isIP)(ip) === 4;
1479
- const maskNum = parseInt(mask, 10);
1480
- const isMaskValid = !isNaN(maskNum) && maskNum >= 0 && maskNum <= 32;
1481
- return isIp4 && isMaskValid;
1482
- }
1483
- return false;
1529
+ function NewResponseObject(data) {
1530
+ const encoder = new TextEncoder();
1531
+ const bytes = encoder.encode(JSON.stringify(data));
1532
+ return {
1533
+ response: bytes,
1534
+ requestid: "",
1535
+ command: "",
1536
+ error: false,
1537
+ errorresponse: {}
1538
+ };
1484
1539
  }
1485
- function isValidIpV6Cidr(input) {
1486
- if (input.includes("/")) {
1487
- const [rawIp, mask] = input.split("/");
1488
- if (!rawIp || !mask) return false;
1489
- const ip = rawIp.split("%")[0].replace(/^\[|\]$/g, "");
1490
- const isIp6 = (0, import_net.isIP)(ip) === 6;
1491
- const maskNum = parseInt(mask, 10);
1492
- const isMaskValid = !isNaN(maskNum) && maskNum >= 0 && maskNum <= 128;
1493
- return isIp6 && isMaskValid;
1540
+ function NewErrorResponseObject(errorResponse) {
1541
+ return {
1542
+ response: new Uint8Array(),
1543
+ requestid: "",
1544
+ command: "",
1545
+ error: true,
1546
+ errorresponse: errorResponse
1547
+ };
1548
+ }
1549
+ function newStatus(tunnelState, errorCode, errorMsg) {
1550
+ let assignedState = tunnelState;
1551
+ if (tunnelState === "live" /* Live */) {
1552
+ assignedState = "running" /* Running */;
1553
+ } else if (tunnelState === "idle" /* New */) {
1554
+ assignedState = "idle" /* New */;
1555
+ } else if (tunnelState === "closed" /* Closed */) {
1556
+ assignedState = "exited" /* Exited */;
1494
1557
  }
1495
- return false;
1558
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1559
+ return {
1560
+ state: assignedState,
1561
+ errorcode: errorCode,
1562
+ errormsg: errorMsg,
1563
+ createdtimestamp: now,
1564
+ starttimestamp: now,
1565
+ endtimestamp: now,
1566
+ warnings: []
1567
+ };
1496
1568
  }
1497
- var import_net;
1498
- var init_extendedOptions = __esm({
1499
- "src/cli/extendedOptions.ts"() {
1569
+ function newStats() {
1570
+ return {
1571
+ numLiveConnections: 0,
1572
+ numTotalConnections: 0,
1573
+ numTotalReqBytes: 0,
1574
+ numTotalResBytes: 0,
1575
+ numTotalTxBytes: 0,
1576
+ elapsedTime: 0
1577
+ };
1578
+ }
1579
+ var TunnelStateType, TunnelErrorCodeType, TunnelWarningCode, ErrorCode, RemoteManagementStatus;
1580
+ var init_types = __esm({
1581
+ "src/types.ts"() {
1500
1582
  "use strict";
1501
1583
  init_cjs_shims();
1502
- import_net = require("net");
1503
- init_logger();
1504
- init_printer();
1584
+ TunnelStateType = /* @__PURE__ */ ((TunnelStateType2) => {
1585
+ TunnelStateType2["New"] = "idle";
1586
+ TunnelStateType2["Starting"] = "starting";
1587
+ TunnelStateType2["Running"] = "running";
1588
+ TunnelStateType2["Live"] = "live";
1589
+ TunnelStateType2["Closed"] = "closed";
1590
+ TunnelStateType2["Exited"] = "exited";
1591
+ return TunnelStateType2;
1592
+ })(TunnelStateType || {});
1593
+ TunnelErrorCodeType = /* @__PURE__ */ ((TunnelErrorCodeType2) => {
1594
+ TunnelErrorCodeType2["NonResponsive"] = "non_responsive";
1595
+ TunnelErrorCodeType2["FailedToConnect"] = "failed_to_connect";
1596
+ TunnelErrorCodeType2["ErrorInAdditionalForwarding"] = "additional_forwarding_error";
1597
+ TunnelErrorCodeType2["WebdebuggerError"] = "webdebugger_error";
1598
+ TunnelErrorCodeType2["NoError"] = "";
1599
+ return TunnelErrorCodeType2;
1600
+ })(TunnelErrorCodeType || {});
1601
+ TunnelWarningCode = /* @__PURE__ */ ((TunnelWarningCode2) => {
1602
+ TunnelWarningCode2["InvalidTunnelServePath"] = "INVALID_TUNNEL_SERVE_PATH";
1603
+ TunnelWarningCode2["UnknownWarning"] = "UNKNOWN_WARNING";
1604
+ return TunnelWarningCode2;
1605
+ })(TunnelWarningCode || {});
1606
+ ErrorCode = {
1607
+ InvalidRequestMethodError: "INVALID_REQUEST_METHOD",
1608
+ InvalidRequestBodyError: "COULD_NOT_READ_BODY",
1609
+ InternalServerError: "INTERNAL_SERVER_ERROR",
1610
+ InvalidBodyFormatError: "INVALID_DATA_FORMAT",
1611
+ ErrorStartingTunnel: "ERROR_STARTING_TUNNEL",
1612
+ TunnelNotFound: "TUNNEL_WITH_ID_OR_CONFIG_ID_NOT_FOUND",
1613
+ TunnelAlreadyRunningError: "TUNNEL_WITH_ID_OR_CONFIG_ID_ALREADY_RUNNING",
1614
+ WebsocketUpgradeFailError: "WEBSOCKET_UPGRADE_FAILED",
1615
+ RemoteManagementAlreadyRunning: "REMOTE_MANAGEMENT_ALREADY_RUNNING",
1616
+ RemoteManagementNotRunning: "REMOTE_MANAGEMENT_NOT_RUNNING",
1617
+ RemoteManagementDeserializationFailed: "REMOTE_MANAGEMENT_DESERIALIZATION_FAILED"
1618
+ };
1619
+ RemoteManagementStatus = {
1620
+ Connecting: "CONNECTING",
1621
+ Disconnecting: "DISCONNECTING",
1622
+ Reconnecting: "RECONNECTING",
1623
+ Running: "RUNNING",
1624
+ NotRunning: "NOT_RUNNING",
1625
+ Error: "ERROR"
1626
+ };
1505
1627
  }
1506
1628
  });
1507
1629
 
1508
- // src/cli/buildConfig.ts
1509
- function parseUserAndDomain(str) {
1510
- let token;
1511
- let type;
1512
- let server;
1513
- let qrCode;
1514
- if (!str) return { token, type, server, qrCode };
1515
- if (str.includes("@")) {
1516
- const [user, domain] = str.split("@", 2);
1517
- if (domainRegex.test(domain)) {
1518
- server = domain;
1519
- const parts = user.split("+");
1520
- for (const part of parts) {
1521
- if ([import_pinggy3.TunnelType.Http, import_pinggy3.TunnelType.Tcp, import_pinggy3.TunnelType.Tls, import_pinggy3.TunnelType.Udp, import_pinggy3.TunnelType.TlsTcp].includes(part.toLowerCase())) {
1522
- type = part;
1523
- } else if (part === "force") {
1524
- token = (token ? token + "+" : "") + part;
1525
- } else if (part === "qr") {
1526
- qrCode = true;
1527
- } else {
1528
- token = (token ? token + "+" : "") + part;
1529
- }
1530
- }
1630
+ // src/remote_management/remote_schema.ts
1631
+ function tunnelConfigToPinggyOptions(config) {
1632
+ return {
1633
+ token: config.token || "",
1634
+ serverAddress: config.serveraddress || "free.pinggy.io",
1635
+ forwarding: `${config.forwardedhost || "localhost"}:${config.localport}`,
1636
+ webDebugger: config.webdebuggerport ? `localhost:${config.webdebuggerport}` : "",
1637
+ ipWhitelist: config.ipwhitelist || [],
1638
+ basicAuth: config.basicauth ? config.basicauth : [],
1639
+ bearerTokenAuth: config.bearerauth ? [config.bearerauth] : [],
1640
+ headerModification: config.headermodification,
1641
+ xForwardedFor: !!config.xff,
1642
+ httpsOnly: config.httpsOnly,
1643
+ originalRequestUrl: config.fullRequestUrl,
1644
+ allowPreflight: config.allowPreflight,
1645
+ reverseProxy: config.noReverseProxy,
1646
+ force: config.force,
1647
+ autoReconnect: config.autoreconnect,
1648
+ optional: {
1649
+ sniServerName: config.localservertlssni || ""
1531
1650
  }
1532
- } else if (domainRegex.test(str)) {
1533
- server = str;
1534
- }
1535
- return { token, type, server, qrCode };
1651
+ };
1536
1652
  }
1537
- function parseUsers(positionalArgs, explicitToken) {
1538
- let token;
1539
- let server;
1540
- let type;
1541
- let forceFlag = false;
1542
- let qrCode = false;
1543
- let remaining = [...positionalArgs];
1544
- if (typeof explicitToken === "string") {
1545
- const parsed = parseUserAndDomain(explicitToken);
1546
- if (parsed.server) server = parsed.server;
1547
- if (parsed.type) type = parsed.type;
1548
- if (parsed.token) token = parsed.token;
1653
+ function pinggyOptionsToTunnelConfig(opts, configid, configName, localserverTls, greetMsg, additionalForwarding, serve) {
1654
+ const forwarding = Array.isArray(opts.forwarding) ? String(opts.forwarding[0].address).replace("//", "").replace(/\/$/, "") : String(opts.forwarding).replace("//", "").replace(/\/$/, "");
1655
+ const parsedForwardedHost = forwarding.split(":").length == 3 ? forwarding.split(":")[1] : forwarding.split(":")[0];
1656
+ const parsedLocalPort = forwarding.split(":").length == 3 ? parseInt(forwarding.split(":")[2], 10) : parseInt(forwarding.split(":")[1], 10);
1657
+ const tunnelType = (Array.isArray(opts.forwarding) ? opts.forwarding[0]?.type : void 0) ?? import_pinggy3.TunnelType.Http;
1658
+ const parsedTokens = opts.bearerTokenAuth ? Array.isArray(opts.bearerTokenAuth) ? opts.bearerTokenAuth : JSON.parse(opts.bearerTokenAuth) : [];
1659
+ return {
1660
+ allowPreflight: opts.allowPreflight ?? false,
1661
+ allowpreflight: opts.allowPreflight ?? false,
1662
+ autoreconnect: opts.autoReconnect ?? false,
1663
+ basicauth: opts.basicAuth && Object.keys(opts.basicAuth).length ? opts.basicAuth : null,
1664
+ bearerauth: parsedTokens.length ? parsedTokens.join(",") : null,
1665
+ configid,
1666
+ configname: configName,
1667
+ greetmsg: greetMsg || "",
1668
+ force: opts.force ?? false,
1669
+ forwardedhost: parsedForwardedHost || "localhost",
1670
+ fullRequestUrl: opts.originalRequestUrl ?? false,
1671
+ headermodification: opts.headerModification || [],
1672
+ //structured list
1673
+ httpsOnly: opts.httpsOnly ?? false,
1674
+ internalwebdebuggerport: 0,
1675
+ ipwhitelist: opts.ipWhitelist ? Array.isArray(opts.ipWhitelist) ? opts.ipWhitelist : JSON.parse(opts.ipWhitelist) : null,
1676
+ localport: parsedLocalPort || 0,
1677
+ localservertlssni: null,
1678
+ regioncode: "",
1679
+ noReverseProxy: opts.reverseProxy ?? false,
1680
+ serveraddress: opts.serverAddress || "free.pinggy.io",
1681
+ serverport: 0,
1682
+ statusCheckInterval: 0,
1683
+ token: opts.token || "",
1684
+ tunnelTimeout: 0,
1685
+ type: tunnelType,
1686
+ webdebuggerport: Number(opts.webDebugger?.split(":")[0]) || 0,
1687
+ xff: opts.xForwardedFor ? "1" : "",
1688
+ localsservertls: localserverTls || false,
1689
+ additionalForwarding: additionalForwarding || [],
1690
+ serve: serve || ""
1691
+ };
1692
+ }
1693
+ var import_pinggy3, import_zod, HeaderModificationSchema, AdditionalForwardingSchema, TunnelConfigSchema, StartSchema, StopSchema, GetSchema, RestartSchema, UpdateConfigSchema;
1694
+ var init_remote_schema = __esm({
1695
+ "src/remote_management/remote_schema.ts"() {
1696
+ "use strict";
1697
+ init_cjs_shims();
1698
+ import_pinggy3 = require("@pinggy/pinggy");
1699
+ import_zod = require("zod");
1700
+ HeaderModificationSchema = import_zod.z.object({
1701
+ key: import_zod.z.string(),
1702
+ value: import_zod.z.array(import_zod.z.string()).optional(),
1703
+ type: import_zod.z.enum(["add", "remove", "update"])
1704
+ });
1705
+ AdditionalForwardingSchema = import_zod.z.object({
1706
+ remoteDomain: import_zod.z.string().optional(),
1707
+ remotePort: import_zod.z.number().optional(),
1708
+ localDomain: import_zod.z.string(),
1709
+ localPort: import_zod.z.number()
1710
+ });
1711
+ TunnelConfigSchema = import_zod.z.object({
1712
+ allowPreflight: import_zod.z.boolean().optional(),
1713
+ // primary key
1714
+ allowpreflight: import_zod.z.boolean().optional(),
1715
+ // legacy key
1716
+ autoreconnect: import_zod.z.boolean(),
1717
+ basicauth: import_zod.z.array(import_zod.z.object({ username: import_zod.z.string(), password: import_zod.z.string() })).nullable(),
1718
+ bearerauth: import_zod.z.string().nullable(),
1719
+ configid: import_zod.z.string(),
1720
+ configname: import_zod.z.string(),
1721
+ greetmsg: import_zod.z.string().optional(),
1722
+ force: import_zod.z.boolean(),
1723
+ forwardedhost: import_zod.z.string(),
1724
+ fullRequestUrl: import_zod.z.boolean(),
1725
+ headermodification: import_zod.z.array(HeaderModificationSchema),
1726
+ httpsOnly: import_zod.z.boolean(),
1727
+ internalwebdebuggerport: import_zod.z.number(),
1728
+ ipwhitelist: import_zod.z.array(import_zod.z.string()).nullable(),
1729
+ localport: import_zod.z.number(),
1730
+ localsservertls: import_zod.z.union([import_zod.z.boolean(), import_zod.z.string()]),
1731
+ localservertlssni: import_zod.z.string().nullable(),
1732
+ regioncode: import_zod.z.string(),
1733
+ noReverseProxy: import_zod.z.boolean(),
1734
+ serveraddress: import_zod.z.string(),
1735
+ serverport: import_zod.z.number(),
1736
+ statusCheckInterval: import_zod.z.number(),
1737
+ token: import_zod.z.string(),
1738
+ tunnelTimeout: import_zod.z.number(),
1739
+ type: import_zod.z.enum([
1740
+ import_pinggy3.TunnelType.Http,
1741
+ import_pinggy3.TunnelType.Tcp,
1742
+ import_pinggy3.TunnelType.Udp,
1743
+ import_pinggy3.TunnelType.Tls,
1744
+ import_pinggy3.TunnelType.TlsTcp
1745
+ ]),
1746
+ webdebuggerport: import_zod.z.number(),
1747
+ xff: import_zod.z.string(),
1748
+ additionalForwarding: import_zod.z.array(AdditionalForwardingSchema).optional(),
1749
+ serve: import_zod.z.string().optional()
1750
+ }).superRefine((data, ctx) => {
1751
+ if (data.allowPreflight === void 0 && data.allowpreflight === void 0) {
1752
+ ctx.addIssue({
1753
+ code: "custom",
1754
+ message: "Either allowPreflight or allowpreflight is required",
1755
+ path: ["allowPreflight"]
1756
+ });
1757
+ }
1758
+ }).transform((data) => ({
1759
+ ...data,
1760
+ allowPreflight: data.allowPreflight ?? data.allowpreflight,
1761
+ allowpreflight: data.allowPreflight ?? data.allowpreflight
1762
+ }));
1763
+ StartSchema = import_zod.z.object({
1764
+ tunnelID: import_zod.z.string().nullable().optional(),
1765
+ tunnelConfig: TunnelConfigSchema
1766
+ });
1767
+ StopSchema = import_zod.z.object({
1768
+ tunnelID: import_zod.z.string().min(1)
1769
+ });
1770
+ GetSchema = StopSchema;
1771
+ RestartSchema = StopSchema;
1772
+ UpdateConfigSchema = import_zod.z.object({
1773
+ tunnelConfig: TunnelConfigSchema
1774
+ });
1549
1775
  }
1550
- if (remaining.length > 0) {
1551
- const first = remaining[0];
1552
- const parsed = parseUserAndDomain(first);
1553
- if (parsed.server) {
1554
- server = parsed.server;
1555
- if (parsed.type) type = parsed.type;
1556
- if (parsed.token) {
1557
- if (parsed.token.includes("+")) {
1558
- const parts = parsed.token.split("+");
1559
- const tOnly = parts.filter((p) => p !== "force").join("+");
1560
- if (tOnly) token = tOnly;
1561
- if (parts.includes("force")) forceFlag = true;
1562
- } else {
1563
- token = parsed.token;
1776
+ });
1777
+
1778
+ // src/remote_management/handler.ts
1779
+ var import_pinggy4, TunnelOperations;
1780
+ var init_handler = __esm({
1781
+ "src/remote_management/handler.ts"() {
1782
+ "use strict";
1783
+ init_cjs_shims();
1784
+ init_types();
1785
+ init_TunnelManager();
1786
+ init_remote_schema();
1787
+ import_pinggy4 = require("@pinggy/pinggy");
1788
+ TunnelOperations = class {
1789
+ constructor() {
1790
+ this.tunnelManager = TunnelManager.getInstance();
1791
+ }
1792
+ buildStatus(tunnelId, state, errorCode) {
1793
+ const status = newStatus(state, errorCode, "");
1794
+ try {
1795
+ const managed = this.tunnelManager.getManagedTunnel("", tunnelId);
1796
+ if (managed) {
1797
+ status.createdtimestamp = managed.createdAt || "";
1798
+ status.starttimestamp = managed.startedAt || "";
1799
+ status.endtimestamp = managed.stoppedAt || "";
1800
+ }
1801
+ } catch (e) {
1564
1802
  }
1803
+ return status;
1565
1804
  }
1566
- if (parsed.qrCode) {
1567
- qrCode = true;
1805
+ // --- Helper to construct TunnelResponse ---
1806
+ async buildTunnelResponse(tunnelid, tunnelConfig, configid, tunnelName, additionalForwarding, serve) {
1807
+ const [status, stats, tlsInfo, greetMsg, remoteurls] = await Promise.all([
1808
+ this.tunnelManager.getTunnelStatus(tunnelid),
1809
+ this.tunnelManager.getLatestTunnelStats(tunnelid) || newStats(),
1810
+ this.tunnelManager.getLocalserverTlsInfo(tunnelid),
1811
+ this.tunnelManager.getTunnelGreetMessage(tunnelid),
1812
+ this.tunnelManager.getTunnelUrls(tunnelid)
1813
+ ]);
1814
+ return {
1815
+ tunnelid,
1816
+ remoteurls,
1817
+ tunnelconfig: pinggyOptionsToTunnelConfig(tunnelConfig, configid, tunnelName, tlsInfo, greetMsg, additionalForwarding),
1818
+ status: this.buildStatus(tunnelid, status, "" /* NoError */),
1819
+ stats
1820
+ };
1568
1821
  }
1569
- remaining = remaining.slice(1);
1570
- }
1571
- }
1572
- return { token, server, type, forceFlag, qrCode, remaining };
1573
- }
1574
- function parseType(finalConfig, values, inferredType) {
1575
- const t = inferredType || values.type || finalConfig.tunnelType;
1576
- if (t === import_pinggy3.TunnelType.Http || t === import_pinggy3.TunnelType.Tcp || t === import_pinggy3.TunnelType.Tls || t === import_pinggy3.TunnelType.Udp || t === import_pinggy3.TunnelType.TlsTcp) {
1577
- finalConfig.tunnelType = [t];
1578
- }
1579
- }
1580
- function parseLocalPort(finalConfig, values) {
1581
- if (typeof values.localport !== "string") return null;
1582
- let lp = values.localport.trim();
1583
- let isHttps = false;
1584
- if (lp.startsWith("https://")) {
1585
- isHttps = true;
1586
- lp = lp.replace(/^https:\/\//, "");
1587
- } else if (lp.startsWith("http://")) {
1588
- lp = lp.replace(/^http:\/\//, "");
1589
- }
1590
- const parts = lp.split(":");
1591
- if (parts.length === 1) {
1592
- const port = parseInt(parts[0], 10);
1593
- if (!Number.isNaN(port) && isValidPort(port)) {
1594
- finalConfig.forwarding = `localhost:${port}`;
1595
- } else {
1596
- return new Error("Invalid local port");
1597
- }
1598
- } else if (parts.length === 2) {
1599
- const host = parts[0] || "localhost";
1600
- const port = parseInt(parts[1], 10);
1601
- if (!Number.isNaN(port) && isValidPort(port)) {
1602
- finalConfig.forwarding = `${host}:${port}`;
1603
- } else {
1604
- return new Error("Invalid local port. Please use -h option for help.");
1605
- }
1606
- } else {
1607
- return new Error("Invalid --localport format. Please use -h option for help.");
1608
- }
1609
- return null;
1610
- }
1611
- function removeIPv6Brackets(ip) {
1612
- if (ip.startsWith("[") && ip.endsWith("]")) {
1613
- return ip.slice(1, -1);
1614
- }
1615
- return ip;
1616
- }
1617
- function ipv6SafeSplitColon(s) {
1618
- const result = [];
1619
- let buf = "";
1620
- const stack = [];
1621
- for (let i = 0; i < s.length; i++) {
1622
- const c = s[i];
1623
- if (c === "[") {
1624
- stack.push(c);
1625
- } else if (c === "]" && stack.length > 0) {
1626
- stack.pop();
1627
- }
1628
- if (c === ":" && stack.length === 0) {
1629
- result.push(buf);
1630
- buf = "";
1631
- } else {
1632
- buf += c;
1633
- }
1634
- }
1635
- result.push(buf);
1636
- return result;
1637
- }
1638
- function parseDefaultForwarding(forwarding) {
1639
- const parts = ipv6SafeSplitColon(forwarding);
1640
- if (parts.length === 3) {
1641
- const remotePort = parseInt(parts[0], 10);
1642
- const localDomain = removeIPv6Brackets(parts[1] || "localhost");
1643
- const localPort = parseInt(parts[2], 10);
1644
- return { remotePort, localDomain, localPort };
1645
- }
1646
- if (parts.length === 4) {
1647
- const remoteDomain = removeIPv6Brackets(parts[0]);
1648
- const remotePort = parseInt(parts[1], 10);
1649
- const localDomain = removeIPv6Brackets(parts[2] || "localhost");
1650
- const localPort = parseInt(parts[3], 10);
1651
- return { remoteDomain, remotePort, localDomain, localPort };
1652
- }
1653
- return new Error("forwarding address incorrect");
1654
- }
1655
- function parseAdditionalForwarding(forwarding) {
1656
- const toPort = (v) => {
1657
- if (!v) return null;
1658
- const n = parseInt(v, 10);
1659
- return Number.isNaN(n) ? null : n;
1660
- };
1661
- const parsed = ipv6SafeSplitColon(forwarding);
1662
- if (parsed.length !== 4) {
1663
- return new Error(
1664
- "forwarding must be in format: [schema//]hostname[/port][@forwardingId]:<placeholder>:<forwardingAddress>:<forwardingPort>"
1665
- );
1666
- }
1667
- const firstPart = parsed[0];
1668
- const [hostPart] = firstPart.split("@");
1669
- let protocol = "http";
1670
- let remoteDomainRaw;
1671
- let remotePort = 0;
1672
- if (hostPart.includes("//")) {
1673
- const [schema, rest] = hostPart.split("//");
1674
- if (!schema || !VALID_PROTOCOLS.includes(schema)) {
1675
- return new Error(`invalid protocol: ${schema}`);
1676
- }
1677
- protocol = schema;
1678
- const domainAndPort = rest.split("/");
1679
- if (domainAndPort.length > 2) {
1680
- return new Error("invalid forwarding address format");
1681
- }
1682
- remoteDomainRaw = domainAndPort[0];
1683
- if (!remoteDomainRaw || !domainRegex.test(remoteDomainRaw)) {
1684
- return new Error("invalid remote domain");
1685
- }
1686
- const parsedRemotePort = toPort(domainAndPort[1]);
1687
- if (protocol === "http") {
1688
- remotePort = 0;
1689
- } else {
1690
- if (parsedRemotePort === null || !isValidPort(parsedRemotePort)) {
1691
- return new Error(
1692
- `${protocol} forwarding requires port in format ${protocol}//domain/remotePort`
1693
- );
1822
+ error(code, err, fallback) {
1823
+ return newErrorResponse({
1824
+ code,
1825
+ message: err instanceof Error ? err.message : fallback
1826
+ });
1694
1827
  }
1695
- remotePort = parsedRemotePort;
1696
- }
1697
- } else {
1698
- remoteDomainRaw = hostPart;
1699
- if (!domainRegex.test(remoteDomainRaw)) {
1700
- return new Error("invalid remote domain");
1701
- }
1702
- protocol = "http";
1703
- remotePort = 0;
1704
- }
1705
- const localDomain = removeIPv6Brackets(parsed[2] || "localhost");
1706
- const localPort = toPort(parsed[3]);
1707
- if (localPort === null || !isValidPort(localPort)) {
1708
- return new Error("forwarding address incorrect: invalid local port");
1709
- }
1710
- return {
1711
- protocol,
1712
- remoteDomain: remoteDomainRaw,
1713
- remotePort,
1714
- localDomain,
1715
- localPort
1716
- };
1717
- }
1718
- function parseReverseTunnelAddr(finalConfig, values) {
1719
- const reverseTunnel = values.R;
1720
- if ((!Array.isArray(reverseTunnel) || reverseTunnel.length === 0) && !values.localport && !finalConfig.forwarding) {
1721
- return new Error("local port not specified. Please use '-h' option for help.");
1722
- }
1723
- if (!Array.isArray(reverseTunnel) || reverseTunnel.length === 0) {
1724
- return null;
1725
- }
1726
- for (const forwarding of reverseTunnel) {
1727
- const slicedForwarding = ipv6SafeSplitColon(forwarding);
1728
- if (slicedForwarding.length === 3) {
1729
- const parsed = parseDefaultForwarding(forwarding);
1730
- if (parsed instanceof Error) return parsed;
1731
- finalConfig.forwarding = `${parsed.localDomain}:${parsed.localPort}`;
1732
- } else if (slicedForwarding.length === 4) {
1733
- finalConfig.additionalForwarding ?? (finalConfig.additionalForwarding = []);
1734
- const parsed = parseAdditionalForwarding(forwarding);
1735
- if (parsed instanceof Error) return parsed;
1736
- finalConfig.additionalForwarding.push(parsed);
1737
- } else {
1738
- return new Error(
1739
- "Incorrect command line arguments: reverse tunnel address incorrect. Please use '-h' option for help."
1740
- );
1741
- }
1742
- }
1743
- return null;
1744
- }
1745
- function parseLocalTunnelAddr(finalConfig, values) {
1746
- if (!Array.isArray(values.L) || values.L.length === 0) return null;
1747
- const firstL = values.L[0];
1748
- const parts = firstL.split(":");
1749
- if (parts.length === 3) {
1750
- const lp = parseInt(parts[2], 10);
1751
- if (!Number.isNaN(lp) && isValidPort(lp)) {
1752
- finalConfig.webDebugger = `localhost:${lp}`;
1753
- } else {
1754
- return new Error(`Invalid debugger port ${lp}`);
1755
- }
1756
- } else {
1757
- return new Error("Incorrect command line arguments: web debugger address incorrect. Please use '-h' option for help.");
1758
- }
1759
- }
1760
- function parseDebugger(finalConfig, values) {
1761
- let dbg = values.debugger;
1762
- if (typeof dbg !== "string") return;
1763
- dbg = dbg.startsWith(":") ? dbg.slice(1) : dbg;
1764
- const d = parseInt(dbg, 10);
1765
- if (!Number.isNaN(d) && isValidPort(d)) {
1766
- finalConfig.webDebugger = `localhost:${d}`;
1767
- } else {
1768
- logger.error("Invalid debugger port:", dbg);
1769
- return new Error(`Invalid debugger port ${dbg}. Please use '-h' option for help.`);
1770
- }
1771
- }
1772
- function parseToken(finalConfig, explicitToken) {
1773
- if (typeof explicitToken === "string" && explicitToken) {
1774
- finalConfig.token = explicitToken;
1775
- }
1776
- }
1777
- function parseArgs(finalConfig, remainingPositionals) {
1778
- parseExtendedOptions(remainingPositionals, finalConfig);
1779
- }
1780
- function storeJson(config, saveconf) {
1781
- if (saveconf) {
1782
- const path5 = saveconf;
1783
- try {
1784
- import_fs3.default.writeFileSync(path5, JSON.stringify(config, null, 2), { encoding: "utf-8", flag: "w" });
1785
- logger.info(`Configuration saved to ${path5}`);
1786
- } catch (err) {
1787
- const msg = err instanceof Error ? err.message : String(err);
1788
- logger.error("Error loading configuration:", msg);
1789
- }
1828
+ // --- Operations ---
1829
+ async handleStart(config) {
1830
+ try {
1831
+ const opts = tunnelConfigToPinggyOptions(config);
1832
+ const additionalForwardingParsed = config.additionalForwarding || [];
1833
+ const { tunnelid, instance, tunnelName, additionalForwarding, serve } = await this.tunnelManager.createTunnel({
1834
+ ...opts,
1835
+ tunnelType: Array.isArray(config.type) ? config.type : config.type ? [config.type] : [import_pinggy4.TunnelType.Http],
1836
+ // Temporary fix in future we will not use this field.
1837
+ configid: config.configid,
1838
+ tunnelName: config.configname,
1839
+ additionalForwarding: additionalForwardingParsed,
1840
+ serve: config.serve
1841
+ });
1842
+ this.tunnelManager.startTunnel(tunnelid);
1843
+ const tunnelPconfig = await this.tunnelManager.getTunnelConfig("", tunnelid);
1844
+ const resp = this.buildTunnelResponse(tunnelid, tunnelPconfig, config.configid, tunnelName, additionalForwarding, serve);
1845
+ return resp;
1846
+ } catch (err) {
1847
+ return this.error(ErrorCode.ErrorStartingTunnel, err, "Unknown error occurred while starting tunnel");
1848
+ }
1849
+ }
1850
+ async handleUpdateConfig(config) {
1851
+ try {
1852
+ const opts = tunnelConfigToPinggyOptions(config);
1853
+ const tunnel = await this.tunnelManager.updateConfig({
1854
+ ...opts,
1855
+ tunnelType: Array.isArray(config.type) ? config.type : config.type ? [config.type] : [import_pinggy4.TunnelType.Http],
1856
+ // // Temporary fix in future we will not use this field.
1857
+ configid: config.configid,
1858
+ tunnelName: config.configname,
1859
+ additionalForwarding: config.additionalForwarding || [],
1860
+ serve: config.serve
1861
+ });
1862
+ if (!tunnel.instance || !tunnel.tunnelConfig)
1863
+ throw new Error("Invalid tunnel state after configuration update");
1864
+ return this.buildTunnelResponse(tunnel.tunnelid, tunnel.tunnelConfig, config.configid, tunnel.tunnelName, tunnel.additionalForwarding, tunnel.serve);
1865
+ } catch (err) {
1866
+ return this.error(ErrorCode.InternalServerError, err, "Failed to update tunnel configuration");
1867
+ }
1868
+ }
1869
+ async handleList() {
1870
+ try {
1871
+ const tunnels = await this.tunnelManager.getAllTunnels();
1872
+ if (tunnels.length === 0) {
1873
+ return [];
1874
+ }
1875
+ return Promise.all(
1876
+ tunnels.map(async (t) => {
1877
+ const rawStats = this.tunnelManager.getLatestTunnelStats(t.tunnelid) || newStats();
1878
+ const [status, tlsInfo, greetMsg] = await Promise.all([
1879
+ this.tunnelManager.getTunnelStatus(t.tunnelid),
1880
+ this.tunnelManager.getLocalserverTlsInfo(t.tunnelid),
1881
+ this.tunnelManager.getTunnelGreetMessage(t.tunnelid)
1882
+ ]);
1883
+ const pinggyOptions = status !== "closed" /* Closed */ && status !== "exited" /* Exited */ ? await this.tunnelManager.getTunnelConfig("", t.tunnelid) : t.tunnelConfig;
1884
+ const tunnelConfig = pinggyOptionsToTunnelConfig(pinggyOptions, t.configid, t.tunnelName, tlsInfo, greetMsg, t.additionalForwarding, t.serve);
1885
+ return {
1886
+ tunnelid: t.tunnelid,
1887
+ remoteurls: t.remoteurls,
1888
+ status: this.buildStatus(t.tunnelid, status, "" /* NoError */),
1889
+ stats: rawStats,
1890
+ tunnelconfig: tunnelConfig
1891
+ };
1892
+ })
1893
+ );
1894
+ } catch (err) {
1895
+ return this.error(ErrorCode.InternalServerError, err, "Failed to list tunnels");
1896
+ }
1897
+ }
1898
+ async handleStop(tunnelid) {
1899
+ try {
1900
+ const { configid } = this.tunnelManager.stopTunnel(tunnelid);
1901
+ const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
1902
+ if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
1903
+ return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, configid, managed.tunnelName, managed.additionalForwarding, managed.serve);
1904
+ } catch (err) {
1905
+ return this.error(ErrorCode.TunnelNotFound, err, "Failed to stop tunnel");
1906
+ }
1907
+ }
1908
+ async handleGet(tunnelid) {
1909
+ try {
1910
+ const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
1911
+ if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
1912
+ return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, managed.configid, managed.tunnelName, managed.additionalForwarding, managed.serve);
1913
+ } catch (err) {
1914
+ return this.error(ErrorCode.TunnelNotFound, err, "Failed to get tunnel information");
1915
+ }
1916
+ }
1917
+ async handleRestart(tunnelid) {
1918
+ try {
1919
+ await this.tunnelManager.restartTunnel(tunnelid);
1920
+ const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
1921
+ if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
1922
+ return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, managed.configid, managed.tunnelName, managed.additionalForwarding, managed.serve);
1923
+ } catch (err) {
1924
+ return this.error(ErrorCode.TunnelNotFound, err, "Failed to restart tunnel");
1925
+ }
1926
+ }
1927
+ handleRegisterStatsListener(tunnelid, listener) {
1928
+ this.tunnelManager.registerStatsListener(tunnelid, listener);
1929
+ }
1930
+ handleUnregisterStatsListener(tunnelid, listnerId) {
1931
+ this.tunnelManager.deregisterStatsListener(tunnelid, listnerId);
1932
+ }
1933
+ handleGetTunnelStats(tunnelid) {
1934
+ try {
1935
+ const stats = this.tunnelManager.getTunnelStats(tunnelid);
1936
+ if (!stats) {
1937
+ return [newStats()];
1938
+ }
1939
+ return stats;
1940
+ } catch (err) {
1941
+ return this.error(ErrorCode.TunnelNotFound, err, "Failed to get tunnel stats");
1942
+ }
1943
+ }
1944
+ handleRegisterDisconnectListener(tunnelid, listener) {
1945
+ this.tunnelManager.registerDisconnectListener(tunnelid, listener);
1946
+ }
1947
+ handleRemoveStoppedTunnelByConfigId(configId) {
1948
+ try {
1949
+ return this.tunnelManager.removeStoppedTunnelByConfigId(configId);
1950
+ } catch (err) {
1951
+ return this.error(ErrorCode.InternalServerError, err, "Failed to remove stopped tunnel by configId");
1952
+ }
1953
+ }
1954
+ handleRemoveStoppedTunnelByTunnelId(tunnelId) {
1955
+ try {
1956
+ return this.tunnelManager.removeStoppedTunnelByTunnelId(tunnelId);
1957
+ } catch (err) {
1958
+ return this.error(ErrorCode.InternalServerError, err, "Failed to remove stopped tunnel by tunnelId");
1959
+ }
1960
+ }
1961
+ };
1790
1962
  }
1791
- }
1792
- function loadJsonConfig(config) {
1793
- const configpath = config["conf"];
1794
- if (typeof configpath === "string" && configpath.trim().length > 0) {
1795
- const filepath = import_path3.default.resolve(configpath);
1796
- try {
1797
- const data = import_fs3.default.readFileSync(filepath, { encoding: "utf-8" });
1798
- const json = JSON.parse(data);
1799
- return json;
1800
- } catch (err) {
1801
- logger.error("Error loading configuration:", err);
1963
+ });
1964
+
1965
+ // src/remote_management/websocket_handlers.ts
1966
+ function handleConnectionStatusMessage(firstMessage) {
1967
+ try {
1968
+ const text = typeof firstMessage === "string" ? firstMessage : firstMessage.toString();
1969
+ const cs = JSON.parse(text);
1970
+ if (!cs.success) {
1971
+ const msg = cs.error_msg || "Connection failed";
1972
+ printer_default.warn(`Connection failed: ${msg}`);
1973
+ logger.warn("Remote management connection failed", { error_code: cs.error_code, error_msg: msg });
1974
+ return false;
1802
1975
  }
1976
+ return true;
1977
+ } catch (e) {
1978
+ logger.warn("Failed to parse connection status message", { error: String(e) });
1979
+ return true;
1803
1980
  }
1804
- return null;
1805
- }
1806
- function isSaveConfOption(values) {
1807
- const saveconf = values["saveconf"];
1808
- if (typeof saveconf === "string" && saveconf.trim().length > 0) {
1809
- return saveconf;
1810
- }
1811
- return null;
1812
- }
1813
- function parseServe(finalConfig, values) {
1814
- const sv = values.serve;
1815
- if (typeof sv !== "string" || sv.trim().length === 0) return null;
1816
- finalConfig.serve = sv;
1817
- return null;
1818
- }
1819
- async function buildFinalConfig(values, positionals) {
1820
- let token;
1821
- let server;
1822
- let type;
1823
- let forceFlag = false;
1824
- let qrCode = false;
1825
- let finalConfig = new Object();
1826
- let saveconf = isSaveConfOption(values);
1827
- const configFromFile = loadJsonConfig(values);
1828
- const userParse = parseUsers(positionals, values.token);
1829
- token = userParse.token;
1830
- server = userParse.server;
1831
- type = userParse.type;
1832
- forceFlag = userParse.forceFlag;
1833
- qrCode = userParse.qrCode;
1834
- const remainingPositionals = userParse.remaining;
1835
- const initialTunnel = type || values.type;
1836
- finalConfig = {
1837
- ...defaultOptions,
1838
- ...configFromFile || {},
1839
- // Apply loaded config on top of defaults
1840
- configid: getRandomId(),
1841
- token: token || (configFromFile?.token || (typeof values.token === "string" ? values.token : "")),
1842
- serverAddress: server || (configFromFile?.serverAddress || defaultOptions.serverAddress),
1843
- tunnelType: initialTunnel ? [initialTunnel] : configFromFile?.tunnelType || [import_pinggy3.TunnelType.Http],
1844
- NoTUI: values.notui || (configFromFile?.NoTUI || false),
1845
- qrCode: qrCode || (configFromFile?.qrCode || false),
1846
- autoReconnect: values.autoreconnect || (configFromFile?.autoReconnect || false)
1847
- };
1848
- parseType(finalConfig, values, type);
1849
- parseToken(finalConfig, token || values.token);
1850
- const dbgErr = parseDebugger(finalConfig, values);
1851
- if (dbgErr instanceof Error) throw dbgErr;
1852
- const lpErr = parseLocalPort(finalConfig, values);
1853
- if (lpErr instanceof Error) throw lpErr;
1854
- const rErr = parseReverseTunnelAddr(finalConfig, values);
1855
- if (rErr instanceof Error) throw rErr;
1856
- const lErr = parseLocalTunnelAddr(finalConfig, values);
1857
- if (lErr instanceof Error) throw lErr;
1858
- const serveErr = parseServe(finalConfig, values);
1859
- if (serveErr instanceof Error) throw serveErr;
1860
- if (forceFlag) finalConfig.force = true;
1861
- parseArgs(finalConfig, remainingPositionals);
1862
- storeJson(finalConfig, saveconf);
1863
- return finalConfig;
1864
1981
  }
1865
- var import_pinggy3, import_fs3, import_path3, domainRegex, VALID_PROTOCOLS;
1866
- var init_buildConfig = __esm({
1867
- "src/cli/buildConfig.ts"() {
1982
+ var import_zod2, WebSocketCommandHandler;
1983
+ var init_websocket_handlers = __esm({
1984
+ "src/remote_management/websocket_handlers.ts"() {
1868
1985
  "use strict";
1869
1986
  init_cjs_shims();
1870
- init_defaults();
1871
- init_extendedOptions();
1872
1987
  init_logger();
1873
- init_util();
1874
- import_pinggy3 = require("@pinggy/pinggy");
1875
- import_fs3 = __toESM(require("fs"), 1);
1876
- import_path3 = __toESM(require("path"), 1);
1877
- domainRegex = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
1878
- VALID_PROTOCOLS = ["http", "tcp", "udp", "tls"];
1988
+ init_types();
1989
+ init_handler();
1990
+ init_remote_schema();
1991
+ import_zod2 = __toESM(require("zod"), 1);
1992
+ init_printer();
1993
+ WebSocketCommandHandler = class {
1994
+ constructor() {
1995
+ this.tunnelHandler = new TunnelOperations();
1996
+ }
1997
+ safeParse(text) {
1998
+ if (!text) return void 0;
1999
+ try {
2000
+ return JSON.parse(text);
2001
+ } catch (e) {
2002
+ logger.warn("Invalid JSON payload", { error: String(e), text });
2003
+ return void 0;
2004
+ }
2005
+ }
2006
+ sendResponse(ws, resp) {
2007
+ const payload = {
2008
+ ...resp,
2009
+ response: Buffer.from(resp.response || []).toString("base64")
2010
+ };
2011
+ ws.send(JSON.stringify(payload));
2012
+ }
2013
+ sendError(ws, req, message, code = ErrorCode.InternalServerError) {
2014
+ const resp = NewErrorResponseObject({ code, message });
2015
+ resp.command = req.command || "";
2016
+ resp.requestid = req.requestid || "";
2017
+ this.sendResponse(ws, resp);
2018
+ }
2019
+ async handleStartReq(req, raw) {
2020
+ const dc = StartSchema.parse(raw);
2021
+ printer_default.info("Starting tunnel with config name: " + dc.tunnelConfig.configname);
2022
+ const result = await this.tunnelHandler.handleStart(dc.tunnelConfig);
2023
+ return this.wrapResponse(result, req);
2024
+ }
2025
+ async handleStopReq(req, raw) {
2026
+ const dc = StopSchema.parse(raw);
2027
+ printer_default.info("Stopping tunnel with ID: " + dc.tunnelID);
2028
+ const result = await this.tunnelHandler.handleStop(dc.tunnelID);
2029
+ return this.wrapResponse(result, req);
2030
+ }
2031
+ async handleGetReq(req, raw) {
2032
+ const dc = GetSchema.parse(raw);
2033
+ const result = await this.tunnelHandler.handleGet(dc.tunnelID);
2034
+ return this.wrapResponse(result, req);
2035
+ }
2036
+ async handleRestartReq(req, raw) {
2037
+ const dc = RestartSchema.parse(raw);
2038
+ const result = await this.tunnelHandler.handleRestart(dc.tunnelID);
2039
+ return this.wrapResponse(result, req);
2040
+ }
2041
+ async handleUpdateConfigReq(req, raw) {
2042
+ const dc = UpdateConfigSchema.parse(raw);
2043
+ const result = await this.tunnelHandler.handleUpdateConfig(dc.tunnelConfig);
2044
+ return this.wrapResponse(result, req);
2045
+ }
2046
+ async handleListReq(req) {
2047
+ const result = await this.tunnelHandler.handleList();
2048
+ return this.wrapResponse(result, req);
2049
+ }
2050
+ wrapResponse(result, req) {
2051
+ if (isErrorResponse(result)) {
2052
+ const errResp = NewErrorResponseObject(result);
2053
+ errResp.command = req.command;
2054
+ errResp.requestid = req.requestid;
2055
+ return errResp;
2056
+ }
2057
+ const finalResult = JSON.parse(JSON.stringify(result));
2058
+ if (Array.isArray(finalResult)) {
2059
+ finalResult.forEach((item) => {
2060
+ if (item?.tunnelconfig) {
2061
+ delete item.tunnelconfig.allowPreflight;
2062
+ }
2063
+ });
2064
+ } else if (finalResult?.tunnelconfig) {
2065
+ delete finalResult.tunnelconfig.allowPreflight;
2066
+ }
2067
+ const respObj = NewResponseObject(finalResult);
2068
+ respObj.command = req.command;
2069
+ respObj.requestid = req.requestid;
2070
+ return respObj;
2071
+ }
2072
+ async handle(ws, req) {
2073
+ const cmd = (req.command || "").toLowerCase();
2074
+ const raw = this.safeParse(req.data);
2075
+ try {
2076
+ let response;
2077
+ switch (cmd) {
2078
+ case "start": {
2079
+ response = await this.handleStartReq(req, raw);
2080
+ break;
2081
+ }
2082
+ case "stop": {
2083
+ response = await this.handleStopReq(req, raw);
2084
+ break;
2085
+ }
2086
+ case "get": {
2087
+ response = await this.handleGetReq(req, raw);
2088
+ break;
2089
+ }
2090
+ case "restart": {
2091
+ response = await this.handleRestartReq(req, raw);
2092
+ break;
2093
+ }
2094
+ case "updateconfig": {
2095
+ response = await this.handleUpdateConfigReq(req, raw);
2096
+ break;
2097
+ }
2098
+ case "list": {
2099
+ response = await this.handleListReq(req);
2100
+ break;
2101
+ }
2102
+ default:
2103
+ if (typeof req.command === "string") {
2104
+ logger.warn("Unknown command", { command: req.command });
2105
+ }
2106
+ return this.sendError(ws, req, "Invalid command");
2107
+ }
2108
+ logger.debug("Sending response", { command: response.command, requestid: response.requestid });
2109
+ this.sendResponse(ws, response);
2110
+ } catch (e) {
2111
+ if (e instanceof import_zod2.default.ZodError) {
2112
+ logger.warn("Validation failed", { cmd, issues: e.issues });
2113
+ return this.sendError(ws, req, "Invalid request data", ErrorCode.InvalidBodyFormatError);
2114
+ }
2115
+ logger.error("Error handling command", { cmd, error: String(e) });
2116
+ return this.sendError(ws, req, e?.message || "Internal error");
2117
+ }
2118
+ }
2119
+ };
1879
2120
  }
1880
2121
  });
1881
2122
 
1882
- // src/types.ts
1883
- function isErrorResponse(obj) {
1884
- return typeof obj === "object" && obj !== null && "code" in obj && "message" in obj && typeof obj.message === "string" && Object.values(ErrorCode).includes(obj.code);
1885
- }
1886
- function newErrorResponse(codeOrError, message) {
1887
- if (typeof codeOrError === "object") {
1888
- return codeOrError;
2123
+ // src/remote_management/remoteManagement.ts
2124
+ function buildRemoteManagementWsUrl(manage) {
2125
+ let baseUrl = (manage || "dashboard.pinggy.io").trim();
2126
+ if (!(baseUrl.startsWith("ws://") || baseUrl.startsWith("wss://"))) {
2127
+ baseUrl = "wss://" + baseUrl;
1889
2128
  }
1890
- return {
1891
- code: codeOrError,
1892
- message
1893
- };
2129
+ const trimmed = baseUrl.replace(/\/$/, "");
2130
+ return `${trimmed}/backend/api/v1/remote-management/connect`;
1894
2131
  }
1895
- function NewResponseObject(data) {
1896
- const encoder = new TextEncoder();
1897
- const bytes = encoder.encode(JSON.stringify(data));
1898
- return {
1899
- response: bytes,
1900
- requestid: "",
1901
- command: "",
1902
- error: false,
1903
- errorresponse: {}
1904
- };
2132
+ function extractHostname(u) {
2133
+ try {
2134
+ const url = new URL(u);
2135
+ return url.host;
2136
+ } catch {
2137
+ return u;
2138
+ }
1905
2139
  }
1906
- function NewErrorResponseObject(errorResponse) {
1907
- return {
1908
- response: new Uint8Array(),
1909
- requestid: "",
1910
- command: "",
1911
- error: true,
1912
- errorresponse: errorResponse
1913
- };
2140
+ function sleep(ms) {
2141
+ return new Promise((res) => setTimeout(res, ms));
1914
2142
  }
1915
- function newStatus(tunnelState, errorCode, errorMsg) {
1916
- let assignedState = tunnelState;
1917
- if (tunnelState === "live" /* Live */) {
1918
- assignedState = "running" /* Running */;
1919
- } else if (tunnelState === "idle" /* New */) {
1920
- assignedState = "idle" /* New */;
1921
- } else if (tunnelState === "closed" /* Closed */) {
1922
- assignedState = "exited" /* Exited */;
2143
+ async function parseRemoteManagement(values) {
2144
+ const rmToken = values["remote-management"];
2145
+ if (typeof rmToken === "string" && rmToken.trim().length > 0) {
2146
+ const manageHost = values["manage"];
2147
+ try {
2148
+ await initiateRemoteManagement(rmToken, manageHost);
2149
+ return { ok: true };
2150
+ } catch (e) {
2151
+ logger.error("Failed to initiate remote management:", e);
2152
+ return { ok: false, error: e };
2153
+ }
1923
2154
  }
1924
- const now = (/* @__PURE__ */ new Date()).toISOString();
1925
- return {
1926
- state: assignedState,
1927
- errorcode: errorCode,
1928
- errormsg: errorMsg,
1929
- createdtimestamp: now,
1930
- starttimestamp: now,
1931
- endtimestamp: now,
1932
- warnings: []
1933
- };
1934
- }
1935
- function newStats() {
1936
- return {
1937
- numLiveConnections: 0,
1938
- numTotalConnections: 0,
1939
- numTotalReqBytes: 0,
1940
- numTotalResBytes: 0,
1941
- numTotalTxBytes: 0,
1942
- elapsedTime: 0
1943
- };
1944
2155
  }
1945
- var ErrorCode, RemoteManagementStatus;
1946
- var init_types = __esm({
1947
- "src/types.ts"() {
1948
- "use strict";
1949
- init_cjs_shims();
1950
- ErrorCode = {
1951
- InvalidRequestMethodError: "INVALID_REQUEST_METHOD",
1952
- InvalidRequestBodyError: "COULD_NOT_READ_BODY",
1953
- InternalServerError: "INTERNAL_SERVER_ERROR",
1954
- InvalidBodyFormatError: "INVALID_DATA_FORMAT",
1955
- ErrorStartingTunnel: "ERROR_STARTING_TUNNEL",
1956
- TunnelNotFound: "TUNNEL_WITH_ID_OR_CONFIG_ID_NOT_FOUND",
1957
- TunnelAlreadyRunningError: "TUNNEL_WITH_ID_OR_CONFIG_ID_ALREADY_RUNNING",
1958
- WebsocketUpgradeFailError: "WEBSOCKET_UPGRADE_FAILED",
1959
- RemoteManagementAlreadyRunning: "REMOTE_MANAGEMENT_ALREADY_RUNNING",
1960
- RemoteManagementNotRunning: "REMOTE_MANAGEMENT_NOT_RUNNING",
1961
- RemoteManagementDeserializationFailed: "REMOTE_MANAGEMENT_DESERIALIZATION_FAILED"
1962
- };
1963
- RemoteManagementStatus = {
1964
- Connecting: "CONNECTING",
1965
- Disconnecting: "DISCONNECTING",
1966
- Reconnecting: "RECONNECTING",
1967
- Running: "RUNNING",
1968
- NotRunning: "NOT_RUNNING",
1969
- Error: "ERROR"
1970
- };
2156
+ async function initiateRemoteManagement(token, manage) {
2157
+ if (!token || token.trim().length === 0) {
2158
+ throw new Error("Remote management token is required (use --remote-management <TOKEN>)");
1971
2159
  }
1972
- });
1973
-
1974
- // src/remote_management/remote_schema.ts
1975
- function tunnelConfigToPinggyOptions(config) {
1976
- return {
1977
- token: config.token || "",
1978
- serverAddress: config.serveraddress || "free.pinggy.io",
1979
- forwarding: `${config.forwardedhost || "localhost"}:${config.localport}`,
1980
- webDebugger: config.webdebuggerport ? `localhost:${config.webdebuggerport}` : "",
1981
- ipWhitelist: config.ipwhitelist || [],
1982
- basicAuth: config.basicauth ? config.basicauth : [],
1983
- bearerTokenAuth: config.bearerauth ? [config.bearerauth] : [],
1984
- headerModification: config.headermodification,
1985
- xForwardedFor: !!config.xff,
1986
- httpsOnly: config.httpsOnly,
1987
- originalRequestUrl: config.fullRequestUrl,
1988
- allowPreflight: config.allowPreflight,
1989
- reverseProxy: config.noReverseProxy,
1990
- force: config.force,
1991
- autoReconnect: config.autoreconnect,
1992
- optional: {
1993
- sniServerName: config.localservertlssni || ""
1994
- }
2160
+ const wsUrl = buildRemoteManagementWsUrl(manage);
2161
+ const wsHost = extractHostname(wsUrl);
2162
+ logger.info("Remote management mode enabled.");
2163
+ _stopRequested = false;
2164
+ const sigintHandler = () => {
2165
+ _stopRequested = true;
1995
2166
  };
1996
- }
1997
- function pinggyOptionsToTunnelConfig(opts, configid, configName, localserverTls, greetMsg, additionalForwarding, serve) {
1998
- const forwarding = Array.isArray(opts.forwarding) ? String(opts.forwarding[0].address).replace("//", "").replace(/\/$/, "") : String(opts.forwarding).replace("//", "").replace(/\/$/, "");
1999
- const parsedForwardedHost = forwarding.split(":").length == 3 ? forwarding.split(":")[1] : forwarding.split(":")[0];
2000
- const parsedLocalPort = forwarding.split(":").length == 3 ? parseInt(forwarding.split(":")[2], 10) : parseInt(forwarding.split(":")[1], 10);
2001
- const tunnelType = (Array.isArray(opts.forwarding) ? opts.forwarding[0]?.type : void 0) ?? import_pinggy4.TunnelType.Http;
2002
- const parsedTokens = opts.bearerTokenAuth ? Array.isArray(opts.bearerTokenAuth) ? opts.bearerTokenAuth : JSON.parse(opts.bearerTokenAuth) : [];
2003
- return {
2004
- allowPreflight: opts.allowPreflight ?? false,
2005
- allowpreflight: opts.allowPreflight ?? false,
2006
- autoreconnect: opts.autoReconnect ?? false,
2007
- basicauth: opts.basicAuth && Object.keys(opts.basicAuth).length ? opts.basicAuth : null,
2008
- bearerauth: parsedTokens.length ? parsedTokens.join(",") : null,
2009
- configid,
2010
- configname: configName,
2011
- greetmsg: greetMsg || "",
2012
- force: opts.force ?? false,
2013
- forwardedhost: parsedForwardedHost || "localhost",
2014
- fullRequestUrl: opts.originalRequestUrl ?? false,
2015
- headermodification: opts.headerModification || [],
2016
- //structured list
2017
- httpsOnly: opts.httpsOnly ?? false,
2018
- internalwebdebuggerport: 0,
2019
- ipwhitelist: opts.ipWhitelist ? Array.isArray(opts.ipWhitelist) ? opts.ipWhitelist : JSON.parse(opts.ipWhitelist) : null,
2020
- localport: parsedLocalPort || 0,
2021
- localservertlssni: null,
2022
- regioncode: "",
2023
- noReverseProxy: opts.reverseProxy ?? false,
2024
- serveraddress: opts.serverAddress || "free.pinggy.io",
2025
- serverport: 0,
2026
- statusCheckInterval: 0,
2027
- token: opts.token || "",
2028
- tunnelTimeout: 0,
2029
- type: tunnelType,
2030
- webdebuggerport: Number(opts.webDebugger?.split(":")[0]) || 0,
2031
- xff: opts.xForwardedFor ? "1" : "",
2032
- localsservertls: localserverTls || false,
2033
- additionalForwarding: additionalForwarding || [],
2034
- serve: serve || ""
2167
+ process.once("SIGINT", sigintHandler);
2168
+ const logConnecting = () => {
2169
+ printer_default.print(`Connecting to ${wsHost}`);
2170
+ logger.info("Connecting to remote management", { wsUrl });
2035
2171
  };
2172
+ while (!_stopRequested) {
2173
+ logConnecting();
2174
+ setRemoteManagementState({ status: RemoteManagementStatus.Connecting, errorMessage: "" });
2175
+ try {
2176
+ await handleWebSocketConnection(wsUrl, wsHost, token);
2177
+ } catch (error) {
2178
+ logger.warn("Remote management connection error", { error: String(error) });
2179
+ }
2180
+ if (_stopRequested) break;
2181
+ printer_default.warn(`Remote management disconnected. Reconnecting in ${RECONNECT_SLEEP_MS / 1e3} seconds...`);
2182
+ logger.info("Reconnecting to remote management after disconnect");
2183
+ await sleep(RECONNECT_SLEEP_MS);
2184
+ }
2185
+ process.removeListener("SIGINT", sigintHandler);
2186
+ logger.info("Remote management stopped.");
2187
+ return getRemoteManagementState();
2036
2188
  }
2037
- var import_pinggy4, import_zod, HeaderModificationSchema, AdditionalForwardingSchema, TunnelConfigSchema, StartSchema, StopSchema, GetSchema, RestartSchema, UpdateConfigSchema;
2038
- var init_remote_schema = __esm({
2039
- "src/remote_management/remote_schema.ts"() {
2040
- "use strict";
2041
- init_cjs_shims();
2042
- import_pinggy4 = require("@pinggy/pinggy");
2043
- import_zod = require("zod");
2044
- HeaderModificationSchema = import_zod.z.object({
2045
- key: import_zod.z.string(),
2046
- value: import_zod.z.array(import_zod.z.string()).optional(),
2047
- type: import_zod.z.enum(["add", "remove", "update"])
2189
+ async function handleWebSocketConnection(wsUrl, wsHost, token) {
2190
+ return new Promise((resolve) => {
2191
+ const ws = new import_ws.default(wsUrl, {
2192
+ headers: { Authorization: `Bearer ${token}` }
2048
2193
  });
2049
- AdditionalForwardingSchema = import_zod.z.object({
2050
- remoteDomain: import_zod.z.string().optional(),
2051
- remotePort: import_zod.z.number().optional(),
2052
- localDomain: import_zod.z.string(),
2053
- localPort: import_zod.z.number()
2194
+ currentWs = ws;
2195
+ let heartbeat = null;
2196
+ let firstMessage = true;
2197
+ const cleanup = () => {
2198
+ if (heartbeat) clearInterval(heartbeat);
2199
+ currentWs = null;
2200
+ resolve();
2201
+ };
2202
+ ws.once("open", () => {
2203
+ printer_default.success(`Connected to ${wsHost}`);
2204
+ heartbeat = setInterval(() => {
2205
+ if (ws.readyState === import_ws.default.OPEN) ws.ping();
2206
+ }, PING_INTERVAL_MS);
2207
+ });
2208
+ ws.on("ping", () => ws.pong());
2209
+ ws.on("message", async (data) => {
2210
+ try {
2211
+ if (firstMessage) {
2212
+ firstMessage = false;
2213
+ const ok = handleConnectionStatusMessage(data);
2214
+ if (!ok) ws.close();
2215
+ return;
2216
+ }
2217
+ setRemoteManagementState({ status: RemoteManagementStatus.Running, errorMessage: "" });
2218
+ const req = JSON.parse(data.toString("utf8"));
2219
+ await new WebSocketCommandHandler().handle(ws, req);
2220
+ } catch (e) {
2221
+ logger.warn("Failed handling websocket message", { error: String(e) });
2222
+ }
2054
2223
  });
2055
- TunnelConfigSchema = import_zod.z.object({
2056
- allowPreflight: import_zod.z.boolean().optional(),
2057
- // primary key
2058
- allowpreflight: import_zod.z.boolean().optional(),
2059
- // legacy key
2060
- autoreconnect: import_zod.z.boolean(),
2061
- basicauth: import_zod.z.array(import_zod.z.object({ username: import_zod.z.string(), password: import_zod.z.string() })).nullable(),
2062
- bearerauth: import_zod.z.string().nullable(),
2063
- configid: import_zod.z.string(),
2064
- configname: import_zod.z.string(),
2065
- greetmsg: import_zod.z.string().optional(),
2066
- force: import_zod.z.boolean(),
2067
- forwardedhost: import_zod.z.string(),
2068
- fullRequestUrl: import_zod.z.boolean(),
2069
- headermodification: import_zod.z.array(HeaderModificationSchema),
2070
- httpsOnly: import_zod.z.boolean(),
2071
- internalwebdebuggerport: import_zod.z.number(),
2072
- ipwhitelist: import_zod.z.array(import_zod.z.string()).nullable(),
2073
- localport: import_zod.z.number(),
2074
- localsservertls: import_zod.z.union([import_zod.z.boolean(), import_zod.z.string()]),
2075
- localservertlssni: import_zod.z.string().nullable(),
2076
- regioncode: import_zod.z.string(),
2077
- noReverseProxy: import_zod.z.boolean(),
2078
- serveraddress: import_zod.z.string(),
2079
- serverport: import_zod.z.number(),
2080
- statusCheckInterval: import_zod.z.number(),
2081
- token: import_zod.z.string(),
2082
- tunnelTimeout: import_zod.z.number(),
2083
- type: import_zod.z.enum([
2084
- import_pinggy4.TunnelType.Http,
2085
- import_pinggy4.TunnelType.Tcp,
2086
- import_pinggy4.TunnelType.Udp,
2087
- import_pinggy4.TunnelType.Tls,
2088
- import_pinggy4.TunnelType.TlsTcp
2089
- ]),
2090
- webdebuggerport: import_zod.z.number(),
2091
- xff: import_zod.z.string(),
2092
- additionalForwarding: import_zod.z.array(AdditionalForwardingSchema).optional(),
2093
- serve: import_zod.z.string().optional()
2094
- }).superRefine((data, ctx) => {
2095
- if (data.allowPreflight === void 0 && data.allowpreflight === void 0) {
2096
- ctx.addIssue({
2097
- code: "custom",
2098
- message: "Either allowPreflight or allowpreflight is required",
2099
- path: ["allowPreflight"]
2100
- });
2224
+ ws.on("unexpected-response", (_, res) => {
2225
+ setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: `HTTP ${res.statusCode}` });
2226
+ if (res.statusCode === 401) {
2227
+ printer_default.error("Unauthorized. Please enter a valid token.");
2228
+ logger.error("Unauthorized (401) on remote management connect");
2229
+ } else {
2230
+ printer_default.warn(`Unexpected HTTP ${res.statusCode}. Retrying...`);
2231
+ logger.warn("Unexpected HTTP response", { statusCode: res.statusCode });
2101
2232
  }
2102
- }).transform((data) => ({
2103
- ...data,
2104
- allowPreflight: data.allowPreflight ?? data.allowpreflight,
2105
- allowpreflight: data.allowPreflight ?? data.allowpreflight
2106
- }));
2107
- StartSchema = import_zod.z.object({
2108
- tunnelID: import_zod.z.string().nullable().optional(),
2109
- tunnelConfig: TunnelConfigSchema
2233
+ ws.close();
2110
2234
  });
2111
- StopSchema = import_zod.z.object({
2112
- tunnelID: import_zod.z.string().min(1)
2235
+ ws.on("close", (code, reason) => {
2236
+ setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: "" });
2237
+ logger.info("WebSocket closed", { code, reason: reason.toString() });
2238
+ printer_default.warn(`Disconnected (code: ${code}). Retrying...`);
2239
+ cleanup();
2113
2240
  });
2114
- GetSchema = StopSchema;
2115
- RestartSchema = StopSchema;
2116
- UpdateConfigSchema = import_zod.z.object({
2117
- tunnelConfig: TunnelConfigSchema
2241
+ ws.on("error", (err) => {
2242
+ setRemoteManagementState({ status: RemoteManagementStatus.Error, errorMessage: err.message });
2243
+ logger.warn("WebSocket error", { error: err.message });
2244
+ printer_default.error(err);
2245
+ cleanup();
2118
2246
  });
2247
+ });
2248
+ }
2249
+ async function closeRemoteManagement(timeoutMs = 1e4) {
2250
+ _stopRequested = true;
2251
+ try {
2252
+ if (currentWs) {
2253
+ try {
2254
+ setRemoteManagementState({ status: RemoteManagementStatus.Disconnecting, errorMessage: "" });
2255
+ currentWs.close();
2256
+ } catch (e) {
2257
+ logger.warn("Error while closing current remote management websocket", { error: String(e) });
2258
+ }
2259
+ }
2260
+ const start = Date.now();
2261
+ while (_remoteManagementState.status === "RUNNING") {
2262
+ if (Date.now() - start > timeoutMs) {
2263
+ logger.warn("Timed out waiting for remote management to stop");
2264
+ break;
2265
+ }
2266
+ await sleep(200);
2267
+ }
2268
+ } finally {
2269
+ currentWs = null;
2270
+ setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: "" });
2271
+ return getRemoteManagementState();
2272
+ }
2273
+ }
2274
+ function getRemoteManagementState() {
2275
+ return _remoteManagementState;
2276
+ }
2277
+ function setRemoteManagementState(state, errorMessage) {
2278
+ _remoteManagementState = {
2279
+ status: state.status,
2280
+ errorMessage: errorMessage || ""
2281
+ };
2282
+ }
2283
+ var import_ws, RECONNECT_SLEEP_MS, PING_INTERVAL_MS, _remoteManagementState, _stopRequested, currentWs;
2284
+ var init_remoteManagement = __esm({
2285
+ "src/remote_management/remoteManagement.ts"() {
2286
+ "use strict";
2287
+ init_cjs_shims();
2288
+ import_ws = __toESM(require("ws"), 1);
2289
+ init_logger();
2290
+ init_websocket_handlers();
2291
+ init_printer();
2292
+ init_types();
2293
+ RECONNECT_SLEEP_MS = 5e3;
2294
+ PING_INTERVAL_MS = 3e4;
2295
+ _remoteManagementState = {
2296
+ status: "NOT_RUNNING",
2297
+ errorMessage: ""
2298
+ };
2299
+ _stopRequested = false;
2300
+ currentWs = null;
2119
2301
  }
2120
2302
  });
2121
2303
 
2122
- // src/remote_management/handler.ts
2123
- var import_pinggy5, TunnelOperations;
2124
- var init_handler = __esm({
2125
- "src/remote_management/handler.ts"() {
2304
+ // src/cli/options.ts
2305
+ var cliOptions;
2306
+ var init_options = __esm({
2307
+ "src/cli/options.ts"() {
2126
2308
  "use strict";
2127
2309
  init_cjs_shims();
2128
- init_types();
2129
- init_TunnelManager();
2130
- init_remote_schema();
2131
- import_pinggy5 = require("@pinggy/pinggy");
2132
- TunnelOperations = class {
2133
- constructor() {
2134
- this.tunnelManager = TunnelManager.getInstance();
2135
- }
2136
- buildStatus(tunnelId, state, errorCode) {
2137
- const status = newStatus(state, errorCode, "");
2138
- try {
2139
- const managed = this.tunnelManager.getManagedTunnel("", tunnelId);
2140
- if (managed) {
2141
- status.createdtimestamp = managed.createdAt || "";
2142
- status.starttimestamp = managed.startedAt || "";
2143
- status.endtimestamp = managed.stoppedAt || "";
2144
- }
2145
- } catch (e) {
2146
- }
2147
- return status;
2148
- }
2149
- // --- Helper to construct TunnelResponse ---
2150
- async buildTunnelResponse(tunnelid, tunnelConfig, configid, tunnelName, additionalForwarding, serve) {
2151
- const [status, stats, tlsInfo, greetMsg, remoteurls] = await Promise.all([
2152
- this.tunnelManager.getTunnelStatus(tunnelid),
2153
- this.tunnelManager.getLatestTunnelStats(tunnelid) || newStats(),
2154
- this.tunnelManager.getLocalserverTlsInfo(tunnelid),
2155
- this.tunnelManager.getTunnelGreetMessage(tunnelid),
2156
- this.tunnelManager.getTunnelUrls(tunnelid)
2157
- ]);
2158
- return {
2159
- tunnelid,
2160
- remoteurls,
2161
- tunnelconfig: pinggyOptionsToTunnelConfig(tunnelConfig, configid, tunnelName, tlsInfo, greetMsg, additionalForwarding),
2162
- status: this.buildStatus(tunnelid, status, "" /* NoError */),
2163
- stats
2164
- };
2165
- }
2166
- error(code, err, fallback) {
2167
- return newErrorResponse({
2168
- code,
2169
- message: err instanceof Error ? err.message : fallback
2170
- });
2171
- }
2172
- // --- Operations ---
2173
- async handleStart(config) {
2174
- try {
2175
- const opts = tunnelConfigToPinggyOptions(config);
2176
- const additionalForwardingParsed = config.additionalForwarding || [];
2177
- const { tunnelid, instance, tunnelName, additionalForwarding, serve } = await this.tunnelManager.createTunnel({
2178
- ...opts,
2179
- tunnelType: Array.isArray(config.type) ? config.type : config.type ? [config.type] : [import_pinggy5.TunnelType.Http],
2180
- // Temporary fix in future we will not use this field.
2181
- configid: config.configid,
2182
- tunnelName: config.configname,
2183
- additionalForwarding: additionalForwardingParsed,
2184
- serve: config.serve
2185
- });
2186
- this.tunnelManager.startTunnel(tunnelid);
2187
- const tunnelPconfig = await this.tunnelManager.getTunnelConfig("", tunnelid);
2188
- const resp = this.buildTunnelResponse(tunnelid, tunnelPconfig, config.configid, tunnelName, additionalForwarding, serve);
2189
- return resp;
2190
- } catch (err) {
2191
- return this.error(ErrorCode.ErrorStartingTunnel, err, "Unknown error occurred while starting tunnel");
2192
- }
2193
- }
2194
- async handleUpdateConfig(config) {
2195
- try {
2196
- const opts = tunnelConfigToPinggyOptions(config);
2197
- const tunnel = await this.tunnelManager.updateConfig({
2198
- ...opts,
2199
- tunnelType: Array.isArray(config.type) ? config.type : config.type ? [config.type] : [import_pinggy5.TunnelType.Http],
2200
- // // Temporary fix in future we will not use this field.
2201
- configid: config.configid,
2202
- tunnelName: config.configname,
2203
- additionalForwarding: config.additionalForwarding || [],
2204
- serve: config.serve
2205
- });
2206
- if (!tunnel.instance || !tunnel.tunnelConfig)
2207
- throw new Error("Invalid tunnel state after configuration update");
2208
- return this.buildTunnelResponse(tunnel.tunnelid, tunnel.tunnelConfig, config.configid, tunnel.tunnelName, tunnel.additionalForwarding, tunnel.serve);
2209
- } catch (err) {
2210
- return this.error(ErrorCode.InternalServerError, err, "Failed to update tunnel configuration");
2310
+ cliOptions = {
2311
+ // SSH-like options
2312
+ R: { type: "string", multiple: true, description: "Local port. Eg. -R0:localhost:3000 will forward tunnel connections to local port 3000." },
2313
+ L: { type: "string", multiple: true, description: "Web Debugger address. Eg. -L4300:localhost:4300 will start web debugger on port 4300." },
2314
+ o: { type: "string", multiple: true, description: "Options", hidden: true },
2315
+ "server-port": { type: "string", short: "p", description: "Pinggy server port. Default: 443" },
2316
+ v4: { type: "boolean", short: "4", description: "IPv4 only", hidden: true },
2317
+ v6: { type: "boolean", short: "6", description: "IPv6 only", hidden: true },
2318
+ // These options appear in the ssh command, but we ignore it in CLI
2319
+ t: { type: "boolean", description: "hidden", hidden: true },
2320
+ T: { type: "boolean", description: "hidden", hidden: true },
2321
+ n: { type: "boolean", description: "hidden", hidden: true },
2322
+ N: { type: "boolean", description: "hidden", hidden: true },
2323
+ // Better options
2324
+ type: { type: "string", description: "Type of the connection. Eg. --type tcp" },
2325
+ localport: { type: "string", short: "l", description: "Takes input as [protocol:][host:]port. Eg. --localport https://localhost:8000 OR -l 3000" },
2326
+ debugger: { type: "string", short: "d", description: "Port for web debugger. Eg. --debugger 4300 OR -d 4300" },
2327
+ token: { type: "string", description: "Token for authentication. Eg. --token TOKEN_VALUE" },
2328
+ // Logging options (CLI overrides env)
2329
+ loglevel: { type: "string", description: "Logging level: ERROR, INFO, DEBUG. Overrides PINGGY_LOG_LEVEL environment variable" },
2330
+ logfile: { type: "string", description: "Path to log file. Overrides PINGGY_LOG_FILE environment variable" },
2331
+ v: { type: "boolean", description: "Print logs to stdout for Cli. Overrides PINGGY_LOG_STDOUT environment variable" },
2332
+ vv: { type: "boolean", description: "Enable detailed logging for the Node.js SDK and Libpinggy, including both info and debug level logs." },
2333
+ vvv: { type: "boolean", description: "Enable all logs from Cli, SDK and internal components." },
2334
+ autoreconnect: { type: "string", short: "a", description: "Automatically reconnect tunnel on failure (enabled by default). Use -a false to disable." },
2335
+ // Save and load config
2336
+ saveconf: { type: "string", description: "Create the configuration file based on the options provided here" },
2337
+ conf: { type: "string", description: "Use the configuration file as base. Other options will be used to override this file" },
2338
+ // File server
2339
+ serve: { type: "string", description: "Start a webserver to serve files from the specified path. Eg --serve /path/to/files" },
2340
+ // Remote Control
2341
+ "remote-management": { type: "string", description: "Enable remote management of tunnels with token. Eg. --remote-management API_KEY" },
2342
+ manage: { type: "string", description: "Provide a server address to manage tunnels. Eg --manage dashboard.pinggy.io" },
2343
+ notui: { type: "boolean", description: "Disable TUI in remote management mode" },
2344
+ // Misc
2345
+ version: { type: "boolean", description: "Print version" },
2346
+ // Help
2347
+ help: { type: "boolean", short: "h", description: "Show this help message" }
2348
+ };
2349
+ }
2350
+ });
2351
+
2352
+ // src/cli/help.ts
2353
+ function printHelpMessage() {
2354
+ console.log("\nPinggy CLI Tool - Create secure tunnels to your localhost.");
2355
+ console.log("\nUsage:");
2356
+ console.log(" pinggy [options] -l <port>\n");
2357
+ console.log("Options:");
2358
+ for (const [key, value] of Object.entries(cliOptions)) {
2359
+ if (value.hidden) continue;
2360
+ const short = "short" in value && value.short ? `-${value.short}, ` : " ";
2361
+ const optType = value.type === "boolean" ? "" : "<value>";
2362
+ console.log(` ${short}--${key.padEnd(17)} ${optType.padEnd(8)} ${value.description}`);
2363
+ }
2364
+ console.log("\nExtended options :");
2365
+ console.log(" x:https Enforce HTTPS only (redirect HTTP to HTTPS)");
2366
+ console.log(" x:noreverseproxy Disable built-in reverse-proxy header injection");
2367
+ console.log(" x:localservertls:host Connect to local HTTPS server with SNI");
2368
+ console.log(" x:passpreflight Pass CORS preflight requests unchanged");
2369
+ console.log(" a:Key:Val Add header");
2370
+ console.log(" u:Key:Val Update header");
2371
+ console.log(" r:Key Remove header");
2372
+ console.log(" b:user:pass Basic auth");
2373
+ console.log(" k:BEARER Bearer token");
2374
+ console.log(" w:192.168.1.0/24 IP whitelist (CIDR)");
2375
+ console.log("\nExamples (User-friendly):");
2376
+ console.log(" pinggy -l 3000 # HTTP(S) tunnel to localhost port 3000");
2377
+ console.log(" pinggy --type tcp -l 22 # TCP tunnel for SSH (port 22)");
2378
+ console.log(" pinggy -l 8080 -d 4300 # HTTP tunnel to port 8080 with debugger running at localhost:4300");
2379
+ console.log(" pinggy --token mytoken -l 3000 # Authenticated tunnel");
2380
+ console.log(" pinggy x:https x:xff -l https://localhost:8443 # HTTPS-only + XFF");
2381
+ console.log(" pinggy w:192.168.1.0/24 -l 8080 # IP whitelist restriction");
2382
+ console.log("\nExamples (SSH-style):");
2383
+ console.log(" pinggy -R0:localhost:3000 # Basic HTTP tunnel");
2384
+ console.log(" pinggy --type tcp -R0:localhost:22 # TCP tunnel for SSH");
2385
+ console.log(" pinggy -R0:localhost:8080 -L4300:localhost:4300 # HTTP tunnel with debugger");
2386
+ console.log(" pinggy tcp@ap.example.com -R0:localhost:22 # TCP tunnel to region\n");
2387
+ }
2388
+ var init_help = __esm({
2389
+ "src/cli/help.ts"() {
2390
+ "use strict";
2391
+ init_cjs_shims();
2392
+ init_options();
2393
+ }
2394
+ });
2395
+
2396
+ // src/cli/defaults.ts
2397
+ var defaultOptions;
2398
+ var init_defaults = __esm({
2399
+ "src/cli/defaults.ts"() {
2400
+ "use strict";
2401
+ init_cjs_shims();
2402
+ defaultOptions = {
2403
+ token: void 0,
2404
+ // No default token
2405
+ serverAddress: "a.pinggy.io",
2406
+ forwarding: "localhost:8000",
2407
+ webDebugger: "",
2408
+ ipWhitelist: [],
2409
+ basicAuth: [],
2410
+ bearerTokenAuth: [],
2411
+ headerModification: [],
2412
+ force: false,
2413
+ xForwardedFor: false,
2414
+ httpsOnly: false,
2415
+ originalRequestUrl: false,
2416
+ allowPreflight: false,
2417
+ reverseProxy: false,
2418
+ autoReconnect: true
2419
+ };
2420
+ }
2421
+ });
2422
+
2423
+ // src/cli/extendedOptions.ts
2424
+ function parseExtendedOptions(options, config) {
2425
+ if (!options) return;
2426
+ for (const opt of options) {
2427
+ const [key, value] = opt.replace(/^"|"$/g, "").split(/:(.+)/).filter(Boolean);
2428
+ switch (key) {
2429
+ case "x":
2430
+ switch (value) {
2431
+ case "https":
2432
+ case "httpsonly":
2433
+ config.httpsOnly = true;
2434
+ break;
2435
+ case "passpreflight":
2436
+ case "allowpreflight":
2437
+ config.allowPreflight = true;
2438
+ break;
2439
+ case "reverseproxy":
2440
+ config.reverseProxy = false;
2441
+ break;
2442
+ case "xff":
2443
+ config.xForwardedFor = true;
2444
+ break;
2445
+ case "fullurl":
2446
+ case "fullrequesturl":
2447
+ config.originalRequestUrl = true;
2448
+ break;
2449
+ default:
2450
+ printer_default.warn(`Unknown extended option "${key}"`);
2451
+ logger.warn(`Warning: Unknown extended option "${key}"`);
2452
+ break;
2211
2453
  }
2212
- }
2213
- async handleList() {
2214
- try {
2215
- const tunnels = await this.tunnelManager.getAllTunnels();
2216
- if (tunnels.length === 0) {
2217
- return [];
2454
+ break;
2455
+ case "w":
2456
+ if (value) {
2457
+ const ips = value.split(",").map((ip) => ip.trim()).filter(Boolean);
2458
+ const invalidIps = ips.filter((ip) => !(isValidIpV4Cidr(ip) || isValidIpV6Cidr(ip)));
2459
+ if (invalidIps.length > 0) {
2460
+ printer_default.warn(`Invalid IP/CIDR(s) in whitelist: ${invalidIps.join(", ")}`);
2461
+ logger.warn(`Warning: Invalid IP/CIDR(s) in whitelist: ${invalidIps.join(", ")}`);
2218
2462
  }
2219
- return Promise.all(
2220
- tunnels.map(async (t) => {
2221
- const rawStats = this.tunnelManager.getLatestTunnelStats(t.tunnelid) || newStats();
2222
- const [status, tlsInfo, greetMsg] = await Promise.all([
2223
- this.tunnelManager.getTunnelStatus(t.tunnelid),
2224
- this.tunnelManager.getLocalserverTlsInfo(t.tunnelid),
2225
- this.tunnelManager.getTunnelGreetMessage(t.tunnelid)
2226
- ]);
2227
- const pinggyOptions = status !== "closed" /* Closed */ && status !== "exited" /* Exited */ ? await this.tunnelManager.getTunnelConfig("", t.tunnelid) : t.tunnelConfig;
2228
- const tunnelConfig = pinggyOptionsToTunnelConfig(pinggyOptions, t.configid, t.tunnelName, tlsInfo, greetMsg, t.additionalForwarding, t.serve);
2229
- return {
2230
- tunnelid: t.tunnelid,
2231
- remoteurls: t.remoteurls,
2232
- status: this.buildStatus(t.tunnelid, status, "" /* NoError */),
2233
- stats: rawStats,
2234
- tunnelconfig: tunnelConfig
2235
- };
2236
- })
2237
- );
2238
- } catch (err) {
2239
- return this.error(ErrorCode.InternalServerError, err, "Failed to list tunnels");
2240
- }
2241
- }
2242
- async handleStop(tunnelid) {
2243
- try {
2244
- const { configid } = this.tunnelManager.stopTunnel(tunnelid);
2245
- const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
2246
- if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
2247
- return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, configid, managed.tunnelName, managed.additionalForwarding, managed.serve);
2248
- } catch (err) {
2249
- return this.error(ErrorCode.TunnelNotFound, err, "Failed to stop tunnel");
2250
- }
2251
- }
2252
- async handleGet(tunnelid) {
2253
- try {
2254
- const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
2255
- if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
2256
- return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, managed.configid, managed.tunnelName, managed.additionalForwarding, managed.serve);
2257
- } catch (err) {
2258
- return this.error(ErrorCode.TunnelNotFound, err, "Failed to get tunnel information");
2463
+ if (!(invalidIps.length > 0)) {
2464
+ config.ipWhitelist = ips;
2465
+ }
2466
+ } else {
2467
+ printer_default.warn(`Extended option "${opt}" for 'w' requires IP(s)`);
2468
+ logger.warn(`Warning: Extended option "${opt}" for 'w' requires IP(s)`);
2259
2469
  }
2260
- }
2261
- async handleRestart(tunnelid) {
2262
- try {
2263
- await this.tunnelManager.restartTunnel(tunnelid);
2264
- const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
2265
- if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
2266
- return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, managed.configid, managed.tunnelName, managed.additionalForwarding, managed.serve);
2267
- } catch (err) {
2268
- return this.error(ErrorCode.TunnelNotFound, err, "Failed to restart tunnel");
2470
+ break;
2471
+ case "k":
2472
+ if (!config.bearerTokenAuth) config.bearerTokenAuth = [];
2473
+ if (value) {
2474
+ config.bearerTokenAuth.push(value);
2475
+ } else {
2476
+ printer_default.warn(`Extended option "${opt}" for 'k' requires a value`);
2477
+ logger.warn(`Warning: Extended option "${opt}" for 'k' requires a value`);
2269
2478
  }
2270
- }
2271
- handleRegisterStatsListener(tunnelid, listener) {
2272
- this.tunnelManager.registerStatsListener(tunnelid, listener);
2273
- }
2274
- handleUnregisterStatsListener(tunnelid, listnerId) {
2275
- this.tunnelManager.deregisterStatsListener(tunnelid, listnerId);
2276
- }
2277
- handleGetTunnelStats(tunnelid) {
2278
- try {
2279
- const stats = this.tunnelManager.getTunnelStats(tunnelid);
2280
- if (!stats) {
2281
- return [newStats()];
2282
- }
2283
- return stats;
2284
- } catch (err) {
2285
- return this.error(ErrorCode.TunnelNotFound, err, "Failed to get tunnel stats");
2479
+ break;
2480
+ case "b":
2481
+ if (value && value.includes(":")) {
2482
+ const [username, password] = value.split(/:(.+)/);
2483
+ if (!config.basicAuth) config.basicAuth = [];
2484
+ config.basicAuth.push({ username, password });
2485
+ } else {
2486
+ printer_default.warn(`Extended option "${opt}" for 'b' requires value in format username:password`);
2487
+ logger.warn(`Warning: Extended option "${opt}" for 'b' requires value in format username:password`);
2286
2488
  }
2287
- }
2288
- handleRegisterDisconnectListener(tunnelid, listener) {
2289
- this.tunnelManager.registerDisconnectListener(tunnelid, listener);
2290
- }
2291
- handleRemoveStoppedTunnelByConfigId(configId) {
2292
- try {
2293
- return this.tunnelManager.removeStoppedTunnelByConfigId(configId);
2294
- } catch (err) {
2295
- return this.error(ErrorCode.InternalServerError, err, "Failed to remove stopped tunnel by configId");
2489
+ break;
2490
+ case "a":
2491
+ if (value && value.includes(":")) {
2492
+ const [key2, val] = value.split(/:(.+)/);
2493
+ if (!config.headerModification) config.headerModification = [];
2494
+ config.headerModification.push({ type: "add", key: key2, value: [val] });
2495
+ } else {
2496
+ printer_default.warn(`Extended option "${opt}" for 'a' requires key:value`);
2497
+ logger.warn(`Warning: Extended option "${opt}" for 'a' requires key:value`);
2296
2498
  }
2297
- }
2298
- handleRemoveStoppedTunnelByTunnelId(tunnelId) {
2299
- try {
2300
- return this.tunnelManager.removeStoppedTunnelByTunnelId(tunnelId);
2301
- } catch (err) {
2302
- return this.error(ErrorCode.InternalServerError, err, "Failed to remove stopped tunnel by tunnelId");
2499
+ break;
2500
+ case "u":
2501
+ if (value && value.includes(":")) {
2502
+ const [key2, val] = value.split(/:(.+)/);
2503
+ if (!config.headerModification) config.headerModification = [];
2504
+ config.headerModification.push({ type: "update", key: key2, value: [val] });
2505
+ } else {
2506
+ printer_default.warn(`Extended option "${opt}" for 'u' requires key:value`);
2507
+ logger.warn(`Warning: Extended option "${opt}" for 'u' requires key:value`);
2303
2508
  }
2304
- }
2305
- };
2306
- }
2307
- });
2308
-
2309
- // src/remote_management/websocket_handlers.ts
2310
- function handleConnectionStatusMessage(firstMessage) {
2311
- try {
2312
- const text = typeof firstMessage === "string" ? firstMessage : firstMessage.toString();
2313
- const cs = JSON.parse(text);
2314
- if (!cs.success) {
2315
- const msg = cs.error_msg || "Connection failed";
2316
- printer_default.warn(`Connection failed: ${msg}`);
2317
- logger.warn("Remote management connection failed", { error_code: cs.error_code, error_msg: msg });
2318
- return false;
2509
+ break;
2510
+ case "r":
2511
+ if (value) {
2512
+ if (!config.headerModification) config.headerModification = [];
2513
+ config.headerModification.push({ type: "remove", key: value });
2514
+ } else {
2515
+ printer_default.warn(`Extended option "${opt}" for 'r' requires a key`);
2516
+ }
2517
+ break;
2518
+ default:
2519
+ printer_default.warn(`Unknown extended option "${key}"`);
2520
+ break;
2319
2521
  }
2320
- return true;
2321
- } catch (e) {
2322
- logger.warn("Failed to parse connection status message", { error: String(e) });
2323
- return true;
2324
2522
  }
2325
2523
  }
2326
- var import_zod2, WebSocketCommandHandler;
2327
- var init_websocket_handlers = __esm({
2328
- "src/remote_management/websocket_handlers.ts"() {
2524
+ function isValidIpV4Cidr(input) {
2525
+ if (input.includes("/")) {
2526
+ const [ip, mask] = input.split("/");
2527
+ if (!ip || !mask) return false;
2528
+ const isIp4 = (0, import_net.isIP)(ip) === 4;
2529
+ const maskNum = parseInt(mask, 10);
2530
+ const isMaskValid = !isNaN(maskNum) && maskNum >= 0 && maskNum <= 32;
2531
+ return isIp4 && isMaskValid;
2532
+ }
2533
+ return false;
2534
+ }
2535
+ function isValidIpV6Cidr(input) {
2536
+ if (input.includes("/")) {
2537
+ const [rawIp, mask] = input.split("/");
2538
+ if (!rawIp || !mask) return false;
2539
+ const ip = rawIp.split("%")[0].replace(/^\[|\]$/g, "");
2540
+ const isIp6 = (0, import_net.isIP)(ip) === 6;
2541
+ const maskNum = parseInt(mask, 10);
2542
+ const isMaskValid = !isNaN(maskNum) && maskNum >= 0 && maskNum <= 128;
2543
+ return isIp6 && isMaskValid;
2544
+ }
2545
+ return false;
2546
+ }
2547
+ var import_net;
2548
+ var init_extendedOptions = __esm({
2549
+ "src/cli/extendedOptions.ts"() {
2329
2550
  "use strict";
2330
2551
  init_cjs_shims();
2552
+ import_net = require("net");
2331
2553
  init_logger();
2332
- init_types();
2333
- init_handler();
2334
- init_remote_schema();
2335
- import_zod2 = __toESM(require("zod"), 1);
2336
2554
  init_printer();
2337
- WebSocketCommandHandler = class {
2338
- constructor() {
2339
- this.tunnelHandler = new TunnelOperations();
2340
- }
2341
- safeParse(text) {
2342
- if (!text) return void 0;
2343
- try {
2344
- return JSON.parse(text);
2345
- } catch (e) {
2346
- logger.warn("Invalid JSON payload", { error: String(e), text });
2347
- return void 0;
2348
- }
2349
- }
2350
- sendResponse(ws, resp) {
2351
- const payload = {
2352
- ...resp,
2353
- response: Buffer.from(resp.response || []).toString("base64")
2354
- };
2355
- ws.send(JSON.stringify(payload));
2356
- }
2357
- sendError(ws, req, message, code = ErrorCode.InternalServerError) {
2358
- const resp = NewErrorResponseObject({ code, message });
2359
- resp.command = req.command || "";
2360
- resp.requestid = req.requestid || "";
2361
- this.sendResponse(ws, resp);
2362
- }
2363
- async handleStartReq(req, raw) {
2364
- const dc = StartSchema.parse(raw);
2365
- printer_default.info("Starting tunnel with config name: " + dc.tunnelConfig.configname);
2366
- const result = await this.tunnelHandler.handleStart(dc.tunnelConfig);
2367
- return this.wrapResponse(result, req);
2368
- }
2369
- async handleStopReq(req, raw) {
2370
- const dc = StopSchema.parse(raw);
2371
- printer_default.info("Stopping tunnel with ID: " + dc.tunnelID);
2372
- const result = await this.tunnelHandler.handleStop(dc.tunnelID);
2373
- return this.wrapResponse(result, req);
2374
- }
2375
- async handleGetReq(req, raw) {
2376
- const dc = GetSchema.parse(raw);
2377
- const result = await this.tunnelHandler.handleGet(dc.tunnelID);
2378
- return this.wrapResponse(result, req);
2379
- }
2380
- async handleRestartReq(req, raw) {
2381
- const dc = RestartSchema.parse(raw);
2382
- const result = await this.tunnelHandler.handleRestart(dc.tunnelID);
2383
- return this.wrapResponse(result, req);
2384
- }
2385
- async handleUpdateConfigReq(req, raw) {
2386
- const dc = UpdateConfigSchema.parse(raw);
2387
- const result = await this.tunnelHandler.handleUpdateConfig(dc.tunnelConfig);
2388
- return this.wrapResponse(result, req);
2389
- }
2390
- async handleListReq(req) {
2391
- const result = await this.tunnelHandler.handleList();
2392
- return this.wrapResponse(result, req);
2393
- }
2394
- wrapResponse(result, req) {
2395
- if (isErrorResponse(result)) {
2396
- const errResp = NewErrorResponseObject(result);
2397
- errResp.command = req.command;
2398
- errResp.requestid = req.requestid;
2399
- return errResp;
2400
- }
2401
- const finalResult = JSON.parse(JSON.stringify(result));
2402
- if (Array.isArray(finalResult)) {
2403
- finalResult.forEach((item) => {
2404
- if (item?.tunnelconfig) {
2405
- delete item.tunnelconfig.allowPreflight;
2406
- }
2407
- });
2408
- } else if (finalResult?.tunnelconfig) {
2409
- delete finalResult.tunnelconfig.allowPreflight;
2555
+ }
2556
+ });
2557
+
2558
+ // src/cli/buildConfig.ts
2559
+ function isKeyword(str) {
2560
+ return KEYWORDS.has(str.toLowerCase());
2561
+ }
2562
+ function parseUserAndDomain(str) {
2563
+ let token;
2564
+ let type;
2565
+ let server;
2566
+ let qrCode;
2567
+ let forceFlag;
2568
+ if (!str) return { token, type, server, qrCode, forceFlag };
2569
+ if (str.includes("@")) {
2570
+ const [user, domain] = str.split("@", 2);
2571
+ if (domainRegex.test(domain)) {
2572
+ let processKeyword2 = function(keyword) {
2573
+ if ([import_pinggy5.TunnelType.Http, import_pinggy5.TunnelType.Tcp, import_pinggy5.TunnelType.Tls, import_pinggy5.TunnelType.Udp, import_pinggy5.TunnelType.TlsTcp].includes(keyword)) {
2574
+ type = keyword;
2575
+ } else if (keyword === "force") {
2576
+ forceFlag = true;
2577
+ } else if (keyword === "qr") {
2578
+ qrCode = true;
2410
2579
  }
2411
- const respObj = NewResponseObject(finalResult);
2412
- respObj.command = req.command;
2413
- respObj.requestid = req.requestid;
2414
- return respObj;
2415
- }
2416
- async handle(ws, req) {
2417
- const cmd = (req.command || "").toLowerCase();
2418
- const raw = this.safeParse(req.data);
2419
- try {
2420
- let response;
2421
- switch (cmd) {
2422
- case "start": {
2423
- response = await this.handleStartReq(req, raw);
2424
- break;
2425
- }
2426
- case "stop": {
2427
- response = await this.handleStopReq(req, raw);
2428
- break;
2429
- }
2430
- case "get": {
2431
- response = await this.handleGetReq(req, raw);
2432
- break;
2433
- }
2434
- case "restart": {
2435
- response = await this.handleRestartReq(req, raw);
2436
- break;
2437
- }
2438
- case "updateconfig": {
2439
- response = await this.handleUpdateConfigReq(req, raw);
2440
- break;
2441
- }
2442
- case "list": {
2443
- response = await this.handleListReq(req);
2444
- break;
2445
- }
2446
- default:
2447
- if (typeof req.command === "string") {
2448
- logger.warn("Unknown command", { command: req.command });
2449
- }
2450
- return this.sendError(ws, req, "Invalid command");
2580
+ };
2581
+ var processKeyword = processKeyword2;
2582
+ server = domain;
2583
+ const parts = user.split("+");
2584
+ if (parts.length === 0) {
2585
+ return { token, type, server, qrCode, forceFlag };
2586
+ }
2587
+ const firstPart = parts[0];
2588
+ if (!isKeyword(firstPart)) {
2589
+ token = firstPart;
2590
+ for (let i = 1; i < parts.length; i++) {
2591
+ const part = parts[i].toLowerCase();
2592
+ if (!isKeyword(part)) {
2593
+ throw new Error(`Invalid user format: unexpected token '${part}' when keywords are expected.`);
2451
2594
  }
2452
- logger.debug("Sending response", { command: response.command, requestid: response.requestid });
2453
- this.sendResponse(ws, response);
2454
- } catch (e) {
2455
- if (e instanceof import_zod2.default.ZodError) {
2456
- logger.warn("Validation failed", { cmd, issues: e.issues });
2457
- return this.sendError(ws, req, "Invalid request data", ErrorCode.InvalidBodyFormatError);
2595
+ processKeyword2(part);
2596
+ }
2597
+ } else {
2598
+ for (const part of parts) {
2599
+ const lowerPart = part.toLowerCase();
2600
+ if (!isKeyword(lowerPart)) {
2601
+ throw new Error(`Invalid user format: unexpected token '${lowerPart}' when keywords are expected.`);
2458
2602
  }
2459
- logger.error("Error handling command", { cmd, error: String(e) });
2460
- return this.sendError(ws, req, e?.message || "Internal error");
2603
+ processKeyword2(lowerPart);
2461
2604
  }
2462
2605
  }
2463
- };
2606
+ }
2607
+ } else if (domainRegex.test(str)) {
2608
+ server = str;
2464
2609
  }
2465
- });
2466
-
2467
- // src/remote_management/remoteManagement.ts
2468
- function buildRemoteManagementWsUrl(manage) {
2469
- let baseUrl = (manage || "dashboard.pinggy.io").trim();
2470
- if (!(baseUrl.startsWith("ws://") || baseUrl.startsWith("wss://"))) {
2471
- baseUrl = "wss://" + baseUrl;
2610
+ return { token, type, server, qrCode, forceFlag };
2611
+ }
2612
+ function parseUsers(positionalArgs, explicitToken) {
2613
+ let token;
2614
+ let server;
2615
+ let type;
2616
+ let forceFlag = false;
2617
+ let qrCode = false;
2618
+ let remaining = [...positionalArgs];
2619
+ if (typeof explicitToken === "string") {
2620
+ const parsed = parseUserAndDomain(explicitToken);
2621
+ if (parsed.server) server = parsed.server;
2622
+ if (parsed.type) type = parsed.type;
2623
+ if (parsed.token) token = parsed.token;
2624
+ if (parsed.forceFlag) forceFlag = true;
2625
+ if (parsed.qrCode) qrCode = true;
2626
+ }
2627
+ if (remaining.length > 0) {
2628
+ const first = remaining[0];
2629
+ const parsed = parseUserAndDomain(first);
2630
+ if (parsed.server) {
2631
+ server = parsed.server;
2632
+ if (parsed.type) type = parsed.type;
2633
+ if (parsed.token) token = parsed.token;
2634
+ if (parsed.forceFlag) forceFlag = true;
2635
+ if (parsed.qrCode) qrCode = true;
2636
+ remaining = remaining.slice(1);
2637
+ }
2638
+ }
2639
+ return { token, server, type, forceFlag, qrCode, remaining };
2640
+ }
2641
+ function parseType(finalConfig, values, inferredType) {
2642
+ const t = inferredType || values.type || finalConfig.tunnelType;
2643
+ if (t === import_pinggy5.TunnelType.Http || t === import_pinggy5.TunnelType.Tcp || t === import_pinggy5.TunnelType.Tls || t === import_pinggy5.TunnelType.Udp || t === import_pinggy5.TunnelType.TlsTcp) {
2644
+ finalConfig.tunnelType = [t];
2645
+ }
2646
+ }
2647
+ function parseLocalPort(finalConfig, values) {
2648
+ if (typeof values.localport !== "string") return null;
2649
+ let lp = values.localport.trim();
2650
+ let isHttps = false;
2651
+ if (lp.startsWith("https://")) {
2652
+ isHttps = true;
2653
+ lp = lp.replace(/^https:\/\//, "");
2654
+ } else if (lp.startsWith("http://")) {
2655
+ lp = lp.replace(/^http:\/\//, "");
2656
+ }
2657
+ const parts = lp.split(":");
2658
+ if (parts.length === 1) {
2659
+ const port = parseInt(parts[0], 10);
2660
+ if (!Number.isNaN(port) && isValidPort(port)) {
2661
+ finalConfig.forwarding = `localhost:${port}`;
2662
+ } else {
2663
+ return new Error("Invalid local port");
2664
+ }
2665
+ } else if (parts.length === 2) {
2666
+ const host = parts[0] || "localhost";
2667
+ const port = parseInt(parts[1], 10);
2668
+ if (!Number.isNaN(port) && isValidPort(port)) {
2669
+ finalConfig.forwarding = `${host}:${port}`;
2670
+ } else {
2671
+ return new Error("Invalid local port. Please use -h option for help.");
2672
+ }
2673
+ } else {
2674
+ return new Error("Invalid --localport format. Please use -h option for help.");
2472
2675
  }
2473
- const trimmed = baseUrl.replace(/\/$/, "");
2474
- return `${trimmed}/backend/api/v1/remote-management/connect`;
2676
+ return null;
2475
2677
  }
2476
- function extractHostname(u) {
2477
- try {
2478
- const url = new URL(u);
2479
- return url.host;
2480
- } catch {
2481
- return u;
2678
+ function removeIPv6Brackets(ip) {
2679
+ if (ip.startsWith("[") && ip.endsWith("]")) {
2680
+ return ip.slice(1, -1);
2482
2681
  }
2682
+ return ip;
2483
2683
  }
2484
- function sleep(ms) {
2485
- return new Promise((res) => setTimeout(res, ms));
2486
- }
2487
- async function parseRemoteManagement(values) {
2488
- const rmToken = values["remote-management"];
2489
- if (typeof rmToken === "string" && rmToken.trim().length > 0) {
2490
- const manageHost = values["manage"];
2491
- try {
2492
- await initiateRemoteManagement(rmToken, manageHost);
2493
- return { ok: true };
2494
- } catch (e) {
2495
- logger.error("Failed to initiate remote management:", e);
2496
- return { ok: false, error: e };
2684
+ function ipv6SafeSplitColon(s) {
2685
+ const result = [];
2686
+ let buf = "";
2687
+ const stack = [];
2688
+ for (let i = 0; i < s.length; i++) {
2689
+ const c = s[i];
2690
+ if (c === "[") {
2691
+ stack.push(c);
2692
+ } else if (c === "]" && stack.length > 0) {
2693
+ stack.pop();
2694
+ }
2695
+ if (c === ":" && stack.length === 0) {
2696
+ result.push(buf);
2697
+ buf = "";
2698
+ } else {
2699
+ buf += c;
2497
2700
  }
2498
2701
  }
2702
+ result.push(buf);
2703
+ return result;
2499
2704
  }
2500
- async function initiateRemoteManagement(token, manage) {
2501
- if (!token || token.trim().length === 0) {
2502
- throw new Error("Remote management token is required (use --remote-management <TOKEN>)");
2705
+ function parseDefaultForwarding(forwarding) {
2706
+ const parts = ipv6SafeSplitColon(forwarding);
2707
+ if (parts.length === 3) {
2708
+ const remotePort = parseInt(parts[0], 10);
2709
+ const localDomain = removeIPv6Brackets(parts[1] || "localhost");
2710
+ const localPort = parseInt(parts[2], 10);
2711
+ return { remotePort, localDomain, localPort };
2503
2712
  }
2504
- const wsUrl = buildRemoteManagementWsUrl(manage);
2505
- const wsHost = extractHostname(wsUrl);
2506
- logger.info("Remote management mode enabled.");
2507
- _stopRequested = false;
2508
- const sigintHandler = () => {
2509
- _stopRequested = true;
2713
+ if (parts.length === 4) {
2714
+ const remoteDomain = removeIPv6Brackets(parts[0]);
2715
+ const remotePort = parseInt(parts[1], 10);
2716
+ const localDomain = removeIPv6Brackets(parts[2] || "localhost");
2717
+ const localPort = parseInt(parts[3], 10);
2718
+ return { remoteDomain, remotePort, localDomain, localPort };
2719
+ }
2720
+ return new Error("forwarding address incorrect");
2721
+ }
2722
+ function parseAdditionalForwarding(forwarding) {
2723
+ const toPort = (v) => {
2724
+ if (!v) return null;
2725
+ const n = parseInt(v, 10);
2726
+ return Number.isNaN(n) ? null : n;
2510
2727
  };
2511
- process.once("SIGINT", sigintHandler);
2512
- const logConnecting = () => {
2513
- printer_default.print(`Connecting to ${wsHost}`);
2514
- logger.info("Connecting to remote management", { wsUrl });
2728
+ const parsed = ipv6SafeSplitColon(forwarding);
2729
+ if (parsed.length !== 4) {
2730
+ return new Error(
2731
+ "forwarding must be in format: [schema//]hostname[/port][@forwardingId]:<placeholder>:<forwardingAddress>:<forwardingPort>"
2732
+ );
2733
+ }
2734
+ const firstPart = parsed[0];
2735
+ const [hostPart] = firstPart.split("@");
2736
+ let protocol = "http";
2737
+ let remoteDomainRaw;
2738
+ let remotePort = 0;
2739
+ if (hostPart.includes("//")) {
2740
+ const [schema, rest] = hostPart.split("//");
2741
+ if (!schema || !VALID_PROTOCOLS.includes(schema)) {
2742
+ return new Error(`invalid protocol: ${schema}`);
2743
+ }
2744
+ protocol = schema;
2745
+ const domainAndPort = rest.split("/");
2746
+ if (domainAndPort.length > 2) {
2747
+ return new Error("invalid forwarding address format");
2748
+ }
2749
+ remoteDomainRaw = domainAndPort[0];
2750
+ if (!remoteDomainRaw || !domainRegex.test(remoteDomainRaw)) {
2751
+ return new Error("invalid remote domain");
2752
+ }
2753
+ const parsedRemotePort = toPort(domainAndPort[1]);
2754
+ if (protocol === "http") {
2755
+ remotePort = 0;
2756
+ } else {
2757
+ if (parsedRemotePort === null || !isValidPort(parsedRemotePort)) {
2758
+ return new Error(
2759
+ `${protocol} forwarding requires port in format ${protocol}//domain/remotePort`
2760
+ );
2761
+ }
2762
+ remotePort = parsedRemotePort;
2763
+ }
2764
+ } else {
2765
+ remoteDomainRaw = hostPart;
2766
+ if (!domainRegex.test(remoteDomainRaw)) {
2767
+ return new Error("invalid remote domain");
2768
+ }
2769
+ protocol = "http";
2770
+ remotePort = 0;
2771
+ }
2772
+ const localDomain = removeIPv6Brackets(parsed[2] || "localhost");
2773
+ const localPort = toPort(parsed[3]);
2774
+ if (localPort === null || !isValidPort(localPort)) {
2775
+ return new Error("forwarding address incorrect: invalid local port");
2776
+ }
2777
+ return {
2778
+ protocol,
2779
+ remoteDomain: remoteDomainRaw,
2780
+ remotePort,
2781
+ localDomain,
2782
+ localPort
2515
2783
  };
2516
- while (!_stopRequested) {
2517
- logConnecting();
2518
- setRemoteManagementState({ status: RemoteManagementStatus.Connecting, errorMessage: "" });
2519
- try {
2520
- await handleWebSocketConnection(wsUrl, wsHost, token);
2521
- } catch (error) {
2522
- logger.warn("Remote management connection error", { error: String(error) });
2784
+ }
2785
+ function parseReverseTunnelAddr(finalConfig, values) {
2786
+ const reverseTunnel = values.R;
2787
+ if ((!Array.isArray(reverseTunnel) || reverseTunnel.length === 0) && !values.localport && !finalConfig.forwarding) {
2788
+ return new Error("local port not specified. Please use '-h' option for help.");
2789
+ }
2790
+ if (!Array.isArray(reverseTunnel) || reverseTunnel.length === 0) {
2791
+ return null;
2792
+ }
2793
+ for (const forwarding of reverseTunnel) {
2794
+ const slicedForwarding = ipv6SafeSplitColon(forwarding);
2795
+ if (slicedForwarding.length === 3) {
2796
+ const parsed = parseDefaultForwarding(forwarding);
2797
+ if (parsed instanceof Error) return parsed;
2798
+ finalConfig.forwarding = `${parsed.localDomain}:${parsed.localPort}`;
2799
+ } else if (slicedForwarding.length === 4) {
2800
+ finalConfig.additionalForwarding ?? (finalConfig.additionalForwarding = []);
2801
+ const parsed = parseAdditionalForwarding(forwarding);
2802
+ if (parsed instanceof Error) return parsed;
2803
+ finalConfig.additionalForwarding.push(parsed);
2804
+ } else {
2805
+ return new Error(
2806
+ "Incorrect command line arguments: reverse tunnel address incorrect. Please use '-h' option for help."
2807
+ );
2523
2808
  }
2524
- if (_stopRequested) break;
2525
- printer_default.warn(`Remote management disconnected. Reconnecting in ${RECONNECT_SLEEP_MS / 1e3} seconds...`);
2526
- logger.info("Reconnecting to remote management after disconnect");
2527
- await sleep(RECONNECT_SLEEP_MS);
2528
2809
  }
2529
- process.removeListener("SIGINT", sigintHandler);
2530
- logger.info("Remote management stopped.");
2531
- return getRemoteManagementState();
2810
+ return null;
2532
2811
  }
2533
- async function handleWebSocketConnection(wsUrl, wsHost, token) {
2534
- return new Promise((resolve) => {
2535
- const ws = new import_ws.default(wsUrl, {
2536
- headers: { Authorization: `Bearer ${token}` }
2537
- });
2538
- currentWs = ws;
2539
- let heartbeat = null;
2540
- let firstMessage = true;
2541
- const cleanup = () => {
2542
- if (heartbeat) clearInterval(heartbeat);
2543
- currentWs = null;
2544
- resolve();
2545
- };
2546
- ws.once("open", () => {
2547
- printer_default.success(`Connected to ${wsHost}`);
2548
- heartbeat = setInterval(() => {
2549
- if (ws.readyState === import_ws.default.OPEN) ws.ping();
2550
- }, PING_INTERVAL_MS);
2551
- });
2552
- ws.on("ping", () => ws.pong());
2553
- ws.on("message", async (data) => {
2554
- try {
2555
- if (firstMessage) {
2556
- firstMessage = false;
2557
- const ok = handleConnectionStatusMessage(data);
2558
- if (!ok) ws.close();
2559
- return;
2560
- }
2561
- setRemoteManagementState({ status: RemoteManagementStatus.Running, errorMessage: "" });
2562
- const req = JSON.parse(data.toString("utf8"));
2563
- await new WebSocketCommandHandler().handle(ws, req);
2564
- } catch (e) {
2565
- logger.warn("Failed handling websocket message", { error: String(e) });
2566
- }
2567
- });
2568
- ws.on("unexpected-response", (_, res) => {
2569
- setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: `HTTP ${res.statusCode}` });
2570
- if (res.statusCode === 401) {
2571
- printer_default.error("Unauthorized. Please enter a valid token.");
2572
- logger.error("Unauthorized (401) on remote management connect");
2573
- } else {
2574
- printer_default.warn(`Unexpected HTTP ${res.statusCode}. Retrying...`);
2575
- logger.warn("Unexpected HTTP response", { statusCode: res.statusCode });
2576
- }
2577
- ws.close();
2578
- });
2579
- ws.on("close", (code, reason) => {
2580
- setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: "" });
2581
- logger.info("WebSocket closed", { code, reason: reason.toString() });
2582
- printer_default.warn(`Disconnected (code: ${code}). Retrying...`);
2583
- cleanup();
2584
- });
2585
- ws.on("error", (err) => {
2586
- setRemoteManagementState({ status: RemoteManagementStatus.Error, errorMessage: err.message });
2587
- logger.warn("WebSocket error", { error: err.message });
2588
- printer_default.error(err);
2589
- cleanup();
2590
- });
2591
- });
2812
+ function parseLocalTunnelAddr(finalConfig, values) {
2813
+ if (!Array.isArray(values.L) || values.L.length === 0) return null;
2814
+ const firstL = values.L[0];
2815
+ const parts = firstL.split(":");
2816
+ if (parts.length === 3) {
2817
+ const lp = parseInt(parts[0], 10);
2818
+ if (!Number.isNaN(lp) && isValidPort(lp)) {
2819
+ finalConfig.webDebugger = `localhost:${lp}`;
2820
+ } else {
2821
+ return new Error(`Invalid debugger port ${lp}`);
2822
+ }
2823
+ } else {
2824
+ return new Error("Incorrect command line arguments: web debugger address incorrect. Please use '-h' option for help.");
2825
+ }
2826
+ }
2827
+ function parseDebugger(finalConfig, values) {
2828
+ let dbg = values.debugger;
2829
+ if (typeof dbg !== "string") return;
2830
+ dbg = dbg.startsWith(":") ? dbg.slice(1) : dbg;
2831
+ const d = parseInt(dbg, 10);
2832
+ if (!Number.isNaN(d) && isValidPort(d)) {
2833
+ finalConfig.webDebugger = `localhost:${d}`;
2834
+ } else {
2835
+ logger.error("Invalid debugger port:", dbg);
2836
+ return new Error(`Invalid debugger port ${dbg}. Please use '-h' option for help.`);
2837
+ }
2592
2838
  }
2593
- async function closeRemoteManagement(timeoutMs = 1e4) {
2594
- _stopRequested = true;
2595
- try {
2596
- if (currentWs) {
2597
- try {
2598
- setRemoteManagementState({ status: RemoteManagementStatus.Disconnecting, errorMessage: "" });
2599
- currentWs.close();
2600
- } catch (e) {
2601
- logger.warn("Error while closing current remote management websocket", { error: String(e) });
2602
- }
2839
+ function parseToken(finalConfig, explicitToken) {
2840
+ if (typeof explicitToken === "string" && explicitToken) {
2841
+ finalConfig.token = explicitToken;
2842
+ }
2843
+ }
2844
+ function parseArgs(finalConfig, remainingPositionals) {
2845
+ parseExtendedOptions(remainingPositionals, finalConfig);
2846
+ }
2847
+ function storeJson(config, saveconf) {
2848
+ if (saveconf) {
2849
+ const path5 = saveconf;
2850
+ try {
2851
+ import_fs3.default.writeFileSync(path5, JSON.stringify(config, null, 2), { encoding: "utf-8", flag: "w" });
2852
+ logger.info(`Configuration saved to ${path5}`);
2853
+ } catch (err) {
2854
+ const msg = err instanceof Error ? err.message : String(err);
2855
+ logger.error("Error loading configuration:", msg);
2603
2856
  }
2604
- const start = Date.now();
2605
- while (_remoteManagementState.status === "RUNNING") {
2606
- if (Date.now() - start > timeoutMs) {
2607
- logger.warn("Timed out waiting for remote management to stop");
2608
- break;
2609
- }
2610
- await sleep(200);
2857
+ }
2858
+ }
2859
+ function loadJsonConfig(config) {
2860
+ const configpath = config["conf"];
2861
+ if (typeof configpath === "string" && configpath.trim().length > 0) {
2862
+ const filepath = import_path3.default.resolve(configpath);
2863
+ try {
2864
+ const data = import_fs3.default.readFileSync(filepath, { encoding: "utf-8" });
2865
+ const json = JSON.parse(data);
2866
+ return json;
2867
+ } catch (err) {
2868
+ logger.error("Error loading configuration:", err);
2611
2869
  }
2612
- } finally {
2613
- currentWs = null;
2614
- setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: "" });
2615
- return getRemoteManagementState();
2616
2870
  }
2871
+ return null;
2617
2872
  }
2618
- function getRemoteManagementState() {
2619
- return _remoteManagementState;
2873
+ function isSaveConfOption(values) {
2874
+ const saveconf = values["saveconf"];
2875
+ if (typeof saveconf === "string" && saveconf.trim().length > 0) {
2876
+ return saveconf;
2877
+ }
2878
+ return null;
2620
2879
  }
2621
- function setRemoteManagementState(state, errorMessage) {
2622
- _remoteManagementState = {
2623
- status: state.status,
2624
- errorMessage: errorMessage || ""
2880
+ function parseServe(finalConfig, values) {
2881
+ const sv = values.serve;
2882
+ if (typeof sv !== "string" || sv.trim().length === 0) return null;
2883
+ finalConfig.serve = sv;
2884
+ return null;
2885
+ }
2886
+ function parseAutoReconnect(finalConfig, values) {
2887
+ const autoReconnectValue = values.autoreconnect;
2888
+ if (typeof autoReconnectValue === "string") {
2889
+ const trimmed = autoReconnectValue.trim().toLowerCase();
2890
+ if (trimmed === "true" || trimmed === "") {
2891
+ finalConfig.autoReconnect = true;
2892
+ } else if (trimmed === "false") {
2893
+ finalConfig.autoReconnect = false;
2894
+ } else {
2895
+ return new Error(`Invalid autoreconnect value: ${autoReconnectValue}. Use true or false.`);
2896
+ }
2897
+ }
2898
+ return null;
2899
+ }
2900
+ async function buildFinalConfig(values, positionals) {
2901
+ let token;
2902
+ let server;
2903
+ let type;
2904
+ let forceFlag = false;
2905
+ let qrCode = false;
2906
+ let finalConfig = new Object();
2907
+ let saveconf = isSaveConfOption(values);
2908
+ const configFromFile = loadJsonConfig(values);
2909
+ const userParse = parseUsers(positionals, values.token);
2910
+ token = userParse.token;
2911
+ server = userParse.server;
2912
+ type = userParse.type;
2913
+ forceFlag = userParse.forceFlag;
2914
+ qrCode = userParse.qrCode;
2915
+ const remainingPositionals = userParse.remaining;
2916
+ const initialTunnel = type || values.type;
2917
+ finalConfig = {
2918
+ ...defaultOptions,
2919
+ ...configFromFile || {},
2920
+ // Apply loaded config on top of defaults
2921
+ configid: getRandomId(),
2922
+ token: token || (configFromFile?.token || (typeof values.token === "string" ? values.token : "")),
2923
+ serverAddress: server || (configFromFile?.serverAddress || defaultOptions.serverAddress),
2924
+ tunnelType: initialTunnel ? [initialTunnel] : configFromFile?.tunnelType || [import_pinggy5.TunnelType.Http],
2925
+ NoTUI: values.notui || (configFromFile?.NoTUI || false),
2926
+ qrCode: qrCode || (configFromFile?.qrCode || false),
2927
+ autoReconnect: configFromFile?.autoReconnect ? configFromFile.autoReconnect : defaultOptions.autoReconnect
2625
2928
  };
2929
+ parseType(finalConfig, values, type);
2930
+ parseToken(finalConfig, token || values.token);
2931
+ const dbgErr = parseDebugger(finalConfig, values);
2932
+ if (dbgErr instanceof Error) throw dbgErr;
2933
+ const lpErr = parseLocalPort(finalConfig, values);
2934
+ if (lpErr instanceof Error) throw lpErr;
2935
+ const rErr = parseReverseTunnelAddr(finalConfig, values);
2936
+ if (rErr instanceof Error) throw rErr;
2937
+ const lErr = parseLocalTunnelAddr(finalConfig, values);
2938
+ if (lErr instanceof Error) throw lErr;
2939
+ const serveErr = parseServe(finalConfig, values);
2940
+ if (serveErr instanceof Error) throw serveErr;
2941
+ const autoReconnectErr = parseAutoReconnect(finalConfig, values);
2942
+ if (autoReconnectErr instanceof Error) throw autoReconnectErr;
2943
+ if (forceFlag) finalConfig.force = true;
2944
+ parseArgs(finalConfig, remainingPositionals);
2945
+ storeJson(finalConfig, saveconf);
2946
+ return finalConfig;
2626
2947
  }
2627
- var import_ws, RECONNECT_SLEEP_MS, PING_INTERVAL_MS, _remoteManagementState, _stopRequested, currentWs;
2628
- var init_remoteManagement = __esm({
2629
- "src/remote_management/remoteManagement.ts"() {
2948
+ var import_pinggy5, import_fs3, import_path3, domainRegex, KEYWORDS, VALID_PROTOCOLS;
2949
+ var init_buildConfig = __esm({
2950
+ "src/cli/buildConfig.ts"() {
2630
2951
  "use strict";
2631
2952
  init_cjs_shims();
2632
- import_ws = __toESM(require("ws"), 1);
2953
+ init_defaults();
2954
+ init_extendedOptions();
2633
2955
  init_logger();
2634
- init_websocket_handlers();
2635
- init_printer();
2636
- init_types();
2637
- RECONNECT_SLEEP_MS = 5e3;
2638
- PING_INTERVAL_MS = 3e4;
2639
- _remoteManagementState = {
2640
- status: "NOT_RUNNING",
2641
- errorMessage: ""
2642
- };
2643
- _stopRequested = false;
2644
- currentWs = null;
2956
+ init_util();
2957
+ import_pinggy5 = require("@pinggy/pinggy");
2958
+ import_fs3 = __toESM(require("fs"), 1);
2959
+ import_path3 = __toESM(require("path"), 1);
2960
+ domainRegex = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
2961
+ KEYWORDS = /* @__PURE__ */ new Set([
2962
+ import_pinggy5.TunnelType.Http,
2963
+ import_pinggy5.TunnelType.Tcp,
2964
+ import_pinggy5.TunnelType.Tls,
2965
+ import_pinggy5.TunnelType.Udp,
2966
+ import_pinggy5.TunnelType.TlsTcp,
2967
+ "force",
2968
+ "qr"
2969
+ ]);
2970
+ VALID_PROTOCOLS = ["http", "tcp", "udp", "tls"];
2645
2971
  }
2646
2972
  });
2647
2973
 
@@ -3444,6 +3770,91 @@ function closeDisconnectModal(screen, manager) {
3444
3770
  manager.inDisconnectView = false;
3445
3771
  screen.render();
3446
3772
  }
3773
+ function showReconnectingModal(screen, manager, retryCnt, message) {
3774
+ if (manager.reconnectModal) {
3775
+ manager.reconnectModal.destroy();
3776
+ manager.reconnectModal = null;
3777
+ }
3778
+ manager.inReconnectView = true;
3779
+ manager.reconnectModal = import_blessed2.default.box({
3780
+ parent: screen,
3781
+ top: "center",
3782
+ left: "center",
3783
+ width: "50%",
3784
+ height: "20%",
3785
+ border: {
3786
+ type: "line"
3787
+ },
3788
+ style: {
3789
+ border: {
3790
+ fg: "yellow"
3791
+ }
3792
+ },
3793
+ padding: { left: 2, right: 2, top: 1, bottom: 1 },
3794
+ tags: true,
3795
+ align: "center",
3796
+ valign: "middle"
3797
+ });
3798
+ const content = `{yellow-fg}{bold}Reconnecting...{/bold}{/yellow-fg}
3799
+
3800
+ ${message || `Attempt #${retryCnt} \u2014 trying to re-establish tunnel...`}
3801
+
3802
+ {gray-fg}Please wait{/gray-fg}`;
3803
+ manager.reconnectModal.setContent(content);
3804
+ manager.reconnectModal.focus();
3805
+ screen.render();
3806
+ }
3807
+ function closeReconnectingModal(screen, manager) {
3808
+ if (manager.reconnectModal) {
3809
+ manager.reconnectModal.destroy();
3810
+ manager.reconnectModal = null;
3811
+ }
3812
+ manager.inReconnectView = false;
3813
+ screen.render();
3814
+ }
3815
+ function showReconnectionFailedModal(screen, manager, retryCnt, onClose) {
3816
+ closeReconnectingModal(screen, manager);
3817
+ manager.inReconnectView = true;
3818
+ manager.reconnectModal = import_blessed2.default.box({
3819
+ parent: screen,
3820
+ top: "center",
3821
+ left: "center",
3822
+ width: "50%",
3823
+ height: "20%",
3824
+ border: {
3825
+ type: "line"
3826
+ },
3827
+ style: {
3828
+ border: {
3829
+ fg: "red"
3830
+ }
3831
+ },
3832
+ padding: { left: 2, right: 2, top: 1, bottom: 1 },
3833
+ tags: true,
3834
+ align: "center",
3835
+ valign: "middle"
3836
+ });
3837
+ const content = `{red-fg}{bold}Reconnection Failed{/bold}{/red-fg}
3838
+
3839
+ Failed to reconnect after ${retryCnt} attempts.
3840
+ Tunnel will be closed.
3841
+
3842
+ {white-bg}{black-fg}Closing in 5 seconds...{/black-fg}{/white-bg}`;
3843
+ manager.reconnectModal.setContent(content);
3844
+ manager.reconnectModal.focus();
3845
+ screen.render();
3846
+ const timeout = setTimeout(() => {
3847
+ closeReconnectingModal(screen, manager);
3848
+ if (onClose) onClose();
3849
+ }, 5e3);
3850
+ const keyHandler = () => {
3851
+ clearTimeout(timeout);
3852
+ closeReconnectingModal(screen, manager);
3853
+ if (onClose) onClose();
3854
+ };
3855
+ manager.reconnectModal.key(["escape", "enter", "space"], keyHandler);
3856
+ screen.key(["escape", "enter", "space"], keyHandler);
3857
+ }
3447
3858
  function showLoadingModal(screen, modalManager, message = "Loading...") {
3448
3859
  if (modalManager.loadingView) return;
3449
3860
  modalManager.loadingBox = import_blessed2.default.box({
@@ -3751,9 +4162,11 @@ var init_TunnelTui = __esm({
3751
4162
  detailModal: null,
3752
4163
  keyBindingsModal: null,
3753
4164
  disconnectModal: null,
4165
+ reconnectModal: null,
3754
4166
  inDetailView: false,
3755
4167
  keyBindingView: false,
3756
4168
  inDisconnectView: false,
4169
+ inReconnectView: false,
3757
4170
  loadingBox: null,
3758
4171
  loadingView: false,
3759
4172
  fetchAbortController: null
@@ -3954,6 +4367,25 @@ Tunnel will be closed.` : info.messages?.join("\n") || "Disconnect request recei
3954
4367
  );
3955
4368
  }
3956
4369
  }
4370
+ updateReconnectingInfo(retryCnt, message) {
4371
+ showReconnectingModal(
4372
+ this.screen,
4373
+ this.modalManager,
4374
+ retryCnt,
4375
+ message
4376
+ );
4377
+ }
4378
+ closeReconnectingInfo() {
4379
+ closeReconnectingModal(this.screen, this.modalManager);
4380
+ }
4381
+ updateReconnectionFailed(retryCnt) {
4382
+ showReconnectionFailedModal(
4383
+ this.screen,
4384
+ this.modalManager,
4385
+ retryCnt,
4386
+ () => this.destroy()
4387
+ );
4388
+ }
3957
4389
  start() {
3958
4390
  this.screen.render();
3959
4391
  }
@@ -4054,6 +4486,35 @@ async function startCli(finalConfig, manager) {
4054
4486
  }
4055
4487
  printer_default.print(import_picocolors3.default.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"));
4056
4488
  printer_default.print(import_picocolors3.default.gray("\nPress Ctrl+C to stop the tunnel.\n"));
4489
+ manager2.registerWillReconnectListener(tunnel.tunnelid, (tunnelId, error, messages) => {
4490
+ if (activeTui) {
4491
+ const msg = messages?.join("\n") || error || "Tunnel disconnected, reconnecting...";
4492
+ activeTui.updateReconnectingInfo(0, msg);
4493
+ } else if (finalConfig.autoReconnect) {
4494
+ printer_default.warn(error || "Tunnel connection reset");
4495
+ printer_default.startSpinner(messages?.join("\n"));
4496
+ }
4497
+ });
4498
+ manager2.registerReconnectingListener(tunnel.tunnelid, (tunnelId, retryCnt) => {
4499
+ if (activeTui) {
4500
+ activeTui.updateReconnectingInfo(retryCnt);
4501
+ } else if (finalConfig.autoReconnect) {
4502
+ printer_default.startSpinner(`Reconnecting to Pinggy (attempt #${retryCnt})`);
4503
+ }
4504
+ });
4505
+ manager2.registerReconnectionCompletedListener(tunnel.tunnelid, (tunnelId, urls) => {
4506
+ if (activeTui) {
4507
+ activeTui.closeReconnectingInfo();
4508
+ }
4509
+ });
4510
+ manager2.registerReconnectionFailedListener(tunnel.tunnelid, (tunnelId, retryCnt) => {
4511
+ if (activeTui) {
4512
+ activeTui.updateReconnectionFailed(retryCnt);
4513
+ } else {
4514
+ printer_default.stopSpinnerFail(`Reconnection failed after ${retryCnt} attempts`);
4515
+ process.exit(1);
4516
+ }
4517
+ });
4057
4518
  manager2.registerDisconnectListener(tunnel.tunnelid, async (tunnelId, error, messages) => {
4058
4519
  if (activeTui) {
4059
4520
  disconnectState = {
@@ -4231,6 +4692,19 @@ var init_main = __esm({
4231
4692
  });
4232
4693
 
4233
4694
  // src/index.ts
4695
+ var index_exports = {};
4696
+ __export(index_exports, {
4697
+ TunnelErrorCodeType: () => TunnelErrorCodeType,
4698
+ TunnelManager: () => TunnelManager,
4699
+ TunnelOperations: () => TunnelOperations,
4700
+ TunnelStateType: () => TunnelStateType,
4701
+ TunnelWarningCode: () => TunnelWarningCode,
4702
+ closeRemoteManagement: () => closeRemoteManagement,
4703
+ enablePackageLogging: () => enablePackageLogging,
4704
+ getRemoteManagementState: () => getRemoteManagementState,
4705
+ initiateRemoteManagement: () => initiateRemoteManagement
4706
+ });
4707
+ module.exports = __toCommonJS(index_exports);
4234
4708
  init_cjs_shims();
4235
4709
 
4236
4710
  // src/utils/detect_vc_redist_on_windows.ts
@@ -4275,52 +4749,21 @@ function checkRegistry() {
4275
4749
  }
4276
4750
  return false;
4277
4751
  }
4278
- function getVCRedistVersion() {
4279
- if (import_os.default.platform() !== "win32") return null;
4280
- try {
4281
- for (const key of REGISTRY_KEYS) {
4282
- const cmd = `reg query "${key}" /v Version 2>nul`;
4283
- const result = (0, import_child_process.execSync)(cmd, { encoding: "utf8" });
4284
- const match = result.match(/Version\s+REG_SZ\s+(\S+)/);
4285
- if (match) {
4286
- return match[1];
4287
- }
4288
- }
4289
- } catch {
4290
- }
4291
- return null;
4292
- }
4293
- function hasVCRedist() {
4294
- if (import_os.default.platform() !== "win32") {
4295
- return true;
4296
- }
4297
- if (checkRegistry()) {
4298
- return true;
4299
- }
4300
- if (checkDLLs()) {
4301
- return true;
4302
- }
4303
- return false;
4304
- }
4305
- function getVCRedistStatus() {
4752
+ function checkVCRedist() {
4306
4753
  if (import_os.default.platform() !== "win32") {
4307
4754
  return {
4308
4755
  required: false,
4309
4756
  installed: true,
4310
- version: null,
4311
- method: "non-windows"
4757
+ message: null
4312
4758
  };
4313
4759
  }
4314
4760
  const registryInstalled = checkRegistry();
4315
4761
  const dllsPresent = checkDLLs();
4316
- const version = getVCRedistVersion();
4762
+ const installed = registryInstalled || dllsPresent;
4317
4763
  return {
4318
4764
  required: true,
4319
- installed: registryInstalled || dllsPresent,
4320
- version,
4321
- registryCheck: registryInstalled,
4322
- dllCheck: dllsPresent,
4323
- method: registryInstalled ? "registry" : dllsPresent ? "dll" : "none"
4765
+ installed,
4766
+ message: installed ? null : "Missing Microsoft Visual C++ Runtime. This application requires the Microsoft Visual C++ Runtime to run on Windows.\n"
4324
4767
  };
4325
4768
  }
4326
4769
  async function openDownloadPage() {
@@ -4343,27 +4786,24 @@ async function openDownloadPage() {
4343
4786
  printer_default.info(url + "\n");
4344
4787
  }
4345
4788
  }
4346
- function getVCRedistMessage() {
4347
- const status = getVCRedistStatus();
4348
- if (!status.required || status.installed) {
4349
- return null;
4350
- }
4351
- return {
4352
- error: true,
4353
- 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"
4354
- };
4355
- }
4356
4789
 
4357
4790
  // src/index.ts
4358
4791
  init_printer();
4792
+ init_TunnelManager();
4793
+ init_handler();
4794
+ init_logger();
4795
+ init_remoteManagement();
4796
+ init_types();
4359
4797
  async function verifyAndLoad() {
4360
- if (process.platform === "win32" && !hasVCRedist()) {
4361
- const msg = getVCRedistMessage();
4362
- printer_default.warn(
4363
- msg?.message ?? "This application requires the Microsoft Visual C++ Runtime on Windows."
4364
- );
4365
- await openDownloadPage();
4366
- process.exit(1);
4798
+ if (process.platform === "win32") {
4799
+ const vcRedist = checkVCRedist();
4800
+ if (!vcRedist.installed) {
4801
+ printer_default.warn(
4802
+ vcRedist.message ?? "This application requires the Microsoft Visual C++ Runtime on Windows."
4803
+ );
4804
+ await openDownloadPage();
4805
+ process.exit(1);
4806
+ }
4367
4807
  }
4368
4808
  await Promise.resolve().then(() => (init_main(), main_exports));
4369
4809
  }
@@ -4371,3 +4811,15 @@ verifyAndLoad().catch((err) => {
4371
4811
  printer_default.error(`Failed to start CLI:, ${err}`);
4372
4812
  process.exit(1);
4373
4813
  });
4814
+ // Annotate the CommonJS export names for ESM import in node:
4815
+ 0 && (module.exports = {
4816
+ TunnelErrorCodeType,
4817
+ TunnelManager,
4818
+ TunnelOperations,
4819
+ TunnelStateType,
4820
+ TunnelWarningCode,
4821
+ closeRemoteManagement,
4822
+ enablePackageLogging,
4823
+ getRemoteManagementState,
4824
+ initiateRemoteManagement
4825
+ });