pinggy 0.3.2 → 0.3.3

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.
@@ -7,6 +7,9 @@ export interface ModalManager {
7
7
  inDetailView: boolean;
8
8
  keyBindingView: boolean;
9
9
  inDisconnectView: boolean;
10
+ loadingBox: blessed.Widgets.BoxElement | null;
11
+ loadingView: boolean;
12
+ fetchAbortController: AbortController | null;
10
13
  }
11
14
 
12
15
  /**
@@ -113,11 +116,13 @@ export function showKeyBindingsModal(
113
116
  {bold}Ctrl+c{/bold} Exit
114
117
 
115
118
  Enter/Return Open selected request
116
- Esc Return to main page
119
+ Esc Return to main page (or close modals)
117
120
  UP (↑) Scroll up the requests
118
121
  Down (↓) Scroll down the requests
119
122
  Left (←) Show qr code for previous url
120
123
  Right (→) Show qr code for next url
124
+ Home Jump to top of requests
125
+ End Jump to bottom of requests
121
126
  Ctrl+c Force Exit
122
127
 
123
128
  {white-bg}{black-fg}Press ESC to close{/black-fg}{/white-bg}`;
@@ -214,3 +219,84 @@ export function closeDisconnectModal(
214
219
  manager.inDisconnectView = false;
215
220
  screen.render();
216
221
  }
222
+
223
+ export function showLoadingModal(
224
+ screen: blessed.Widgets.Screen,
225
+ modalManager: ModalManager,
226
+ message: string = "Loading..."
227
+ ): void {
228
+ if (modalManager.loadingView) return;
229
+
230
+ modalManager.loadingBox = blessed.box({
231
+ parent: screen,
232
+ top: "center",
233
+ left: "center",
234
+ width: "60%",
235
+ height: 8,
236
+ border: { type: "line" },
237
+ style: {
238
+ border: { fg: "yellow" },
239
+ },
240
+ tags: true,
241
+ content: `{center}{yellow-fg}{bold}${message}{/bold}{/yellow-fg}
242
+
243
+ {gray-fg}Press ESC to cancel{/gray-fg}{/center}`,
244
+ valign: "middle",
245
+ });
246
+
247
+ modalManager.loadingView = true;
248
+ screen.render();
249
+ }
250
+
251
+ /**
252
+ * Closes the loading modal
253
+ */
254
+ export function closeLoadingModal(
255
+ screen: blessed.Widgets.Screen,
256
+ modalManager: ModalManager
257
+ ): void {
258
+ if (!modalManager.loadingView || !modalManager.loadingBox) return;
259
+
260
+ modalManager.loadingBox.destroy();
261
+ modalManager.loadingBox = null;
262
+ modalManager.loadingView = false;
263
+ screen.render();
264
+ }
265
+
266
+ /**
267
+ * Shows an error modal with a message
268
+ */
269
+ export function showErrorModal(
270
+ screen: blessed.Widgets.Screen,
271
+ modalManager: ModalManager,
272
+ title: string = "Error",
273
+ message: string
274
+ ): void {
275
+ // Reuse the loading box for error display
276
+ if (modalManager.loadingBox) {
277
+ modalManager.loadingBox.destroy();
278
+ modalManager.loadingBox = null;
279
+ }
280
+
281
+ modalManager.loadingBox = blessed.box({
282
+ parent: screen,
283
+ top: "center",
284
+ left: "center",
285
+ width: "60%",
286
+ height: 9,
287
+ border: { type: "line" },
288
+ style: {
289
+ border: { fg: "red" },
290
+ },
291
+ tags: true,
292
+ content: `{center}{red-fg}{bold}${title}{/bold}{/red-fg}
293
+
294
+ {white-fg}${message}{/white-fg}
295
+
296
+ {gray-fg}Press ESC to close{/gray-fg}{/center}`,
297
+ valign: "middle",
298
+ });
299
+
300
+ modalManager.loadingView = true;
301
+ screen.render();
302
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * TUI Configuration Settings
3
+ */
4
+
5
+ export interface TuiConfig {
6
+ /**
7
+ * Maximum number of request/response pairs to keep in memory.
8
+ * Older requests will be removed when this limit is exceeded.
9
+ * Default: 100
10
+ */
11
+ maxRequestPairs: number;
12
+
13
+ /**
14
+ * Number of visible request items to display in the requests box.
15
+ * Default: 10
16
+ */
17
+ visibleRequestCount: number;
18
+
19
+ /**
20
+ * Margin from the viewport edge when auto-scrolling to keep selector visible.
21
+ * Default: 2
22
+ */
23
+ viewportScrollMargin: number;
24
+
25
+ /**
26
+ * Inactivity timeout in milliseconds to auto-unselect the selected row and adjust viewport to latest request.
27
+ * Default: 10000 (10 seconds)
28
+ */
29
+
30
+ inactivityHttpSelectorTimeoutMs?: number;
31
+ }
32
+
33
+ /**
34
+ * Default TUI configuration values
35
+ */
36
+ export const defaultTuiConfig: TuiConfig = {
37
+ maxRequestPairs: 100,
38
+ visibleRequestCount: 10,
39
+ viewportScrollMargin: 2,
40
+ inactivityHttpSelectorTimeoutMs: 10000,
41
+ };
42
+
43
+ /**
44
+ * Get the current TUI configuration.
45
+ */
46
+ export function getTuiConfig(): TuiConfig {
47
+ return {
48
+ maxRequestPairs: defaultTuiConfig.maxRequestPairs,
49
+ visibleRequestCount:defaultTuiConfig.visibleRequestCount,
50
+ viewportScrollMargin: defaultTuiConfig.viewportScrollMargin,
51
+ inactivityHttpSelectorTimeoutMs: defaultTuiConfig.inactivityHttpSelectorTimeoutMs,
52
+ };
53
+ }
@@ -10,7 +10,8 @@ export interface HeadersResult {
10
10
  */
11
11
  export async function fetchReqResHeaders(
12
12
  baseUrl: string,
13
- key: number
13
+ key: number,
14
+ signal?: AbortSignal
14
15
  ): Promise<HeadersResult> {
15
16
  if (!baseUrl) {
16
17
  return { req: "", res: "" };
@@ -20,16 +21,22 @@ export async function fetchReqResHeaders(
20
21
  const [reqRes, resRes] = await Promise.all([
21
22
  fetch(`http://${baseUrl}/introspec/getrawrequestheader`, {
22
23
  headers: { "X-Introspec-Key": key.toString() },
24
+ signal,
23
25
  }),
24
26
  fetch(`http://${baseUrl}/introspec/getrawresponseheader`, {
25
27
  headers: { "X-Introspec-Key": key.toString() },
28
+ signal,
26
29
  }),
27
30
  ]);
28
31
 
29
32
  const [req, res] = await Promise.all([reqRes.text(), resRes.text()]);
30
33
  return { req, res };
31
34
  } catch (err: any) {
35
+ // Re-throw abort errors so caller can handle cancellation
36
+ if (err?.name === 'AbortError') {
37
+ throw err;
38
+ }
32
39
  logger.error("Error fetching headers:", err.message || err);
33
- return { req: "", res: "" };
40
+ throw err;
34
41
  }
35
42
  }
@@ -1,6 +1,7 @@
1
1
  import WebSocket from "ws";
2
2
  import { ReqResPair, WebDebuggerSocketRequest } from "../../types.js";
3
3
  import { logger } from "../../logger.js";
4
+ import { getTuiConfig } from "./config.js";
4
5
 
5
6
  export interface WebDebuggerConnection {
6
7
  close: () => void;
@@ -15,13 +16,38 @@ export interface WebDebuggerConnection {
15
16
  */
16
17
  export function createWebDebuggerConnection(
17
18
  webDebuggerUrl: string,
18
- onUpdate: (pairs: Map<number, ReqResPair>) => void
19
+ onUpdate: (pairs: ReqResPair[]) => void
19
20
  ): WebDebuggerConnection {
20
21
  const pairs = new Map<number, ReqResPair>();
22
+ const pairKeys: number[] = [];
21
23
  let socket: WebSocket | null = null;
22
24
  let reconnectTimeout: NodeJS.Timeout | null = null;
23
25
  let isStopped = false;
24
26
 
27
+ const config = getTuiConfig();
28
+ const maxPairs = config.maxRequestPairs;
29
+
30
+
31
+ // Trim pairs to keep only the latest maxPairs entries
32
+ const trimPairs = () => {
33
+ while (pairKeys.length > maxPairs) {
34
+ const oldestKey = pairKeys.shift();
35
+ if (oldestKey !== undefined) {
36
+ pairs.delete(oldestKey);
37
+ }
38
+ }
39
+ };
40
+
41
+
42
+ // Add or update a pair and track its key for ordering
43
+ const upsertPair = (key: number, pair: ReqResPair) => {
44
+ if (!pairs.has(key)) {
45
+ pairKeys.push(key);
46
+ }
47
+ pairs.set(key, pair);
48
+ trimPairs();
49
+ };
50
+
25
51
  const connect = () => {
26
52
  const ws = new WebSocket(`ws://${webDebuggerUrl}/introspec/websocket`);
27
53
  socket = ws;
@@ -35,8 +61,8 @@ export function createWebDebuggerConnection(
35
61
  const raw = data.toString();
36
62
  const parsed = JSON.parse(raw);
37
63
  const msg = {
38
- Req: parsed.Req || parsed.req,
39
- Res: parsed.Res || parsed.res,
64
+ Req: parsed.req,
65
+ Res: parsed.res,
40
66
  } as Partial<WebDebuggerSocketRequest>;
41
67
 
42
68
  if (msg.Req) {
@@ -45,11 +71,8 @@ export function createWebDebuggerConnection(
45
71
  const merged: ReqResPair = {
46
72
  request: msg.Req,
47
73
  response: existing?.response,
48
- reqHeaders: existing?.reqHeaders ?? {},
49
- resHeaders: existing?.resHeaders ?? {},
50
- headersLoaded: existing?.headersLoaded ?? false,
51
74
  } as ReqResPair;
52
- pairs.set(key, merged);
75
+ upsertPair(key, merged);
53
76
  }
54
77
 
55
78
  if (msg.Res) {
@@ -58,15 +81,20 @@ export function createWebDebuggerConnection(
58
81
  const merged: ReqResPair = {
59
82
  request: existing?.request ?? ({} as any),
60
83
  response: msg.Res,
61
- reqHeaders: existing?.reqHeaders ?? {},
62
- resHeaders: existing?.resHeaders ?? {},
63
- headersLoaded: existing?.headersLoaded ?? false,
64
84
  } as ReqResPair;
65
- pairs.set(key, merged);
85
+ upsertPair(key, merged);
66
86
  }
67
87
 
68
- // Notify listener with a copy of the pairs map
69
- onUpdate(new Map(pairs));
88
+ // Notify listener with reversed array (latest first)
89
+ const reversedPairs: ReqResPair[] = [];
90
+ for (let i = pairKeys.length - 1; i >= 0; i--) {
91
+ const key = pairKeys[i];
92
+ const pair = pairs.get(key);
93
+ if (pair) {
94
+ reversedPairs.push(pair);
95
+ }
96
+ }
97
+ onUpdate(reversedPairs);
70
98
  } catch (err: any) {
71
99
  logger.error("Error parsing WebSocket message:", err.message || err);
72
100
  }
@@ -6,7 +6,7 @@ export function getStatusColor(status: string): string {
6
6
 
7
7
  switch (true) {
8
8
  case statusCode >= 100 && statusCode < 200:
9
- return "orange";
9
+ return "yellow";
10
10
  case statusCode >= 200 && statusCode < 300:
11
11
  return "green";
12
12
  case statusCode >= 300 && statusCode < 400:
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import { ForwardingEntry, pinggy, TunnelType, type PinggyOptions, type TunnelInstance, type TunnelUsageType } from "@pinggy/pinggy";
17
17
  import { logger } from "../logger.js";
18
- import { AdditionalForwarding, Forwarding, TunnelWarningCode, Warning } from "../types.js";
18
+ import { AdditionalForwarding, TunnelWarningCode, Warning } from "../types.js";
19
19
  import path from "node:path";
20
20
  import { Worker } from "node:worker_threads";
21
21
  import { fileURLToPath } from "node:url";
@@ -240,8 +240,8 @@ export class TunnelManager implements ITunnelManager {
240
240
  for (const rule of additionalForwarding) {
241
241
  if (rule && rule.localDomain && rule.localPort && rule.remoteDomain && isValidPort(rule.localPort)) {
242
242
  const forwardingRule: ForwardingEntry = {
243
- type: TunnelType.Http, // In Future we can make this dynamic based on user input
244
- address: `${rule.localDomain}:${rule.localPort}`,
243
+ type: rule.protocol as TunnelType, // In Future we can make this dynamic based on user input
244
+ address:`${rule.localDomain}:${rule.localPort}`,
245
245
  listenAddress: rule.remotePort && isValidPort(rule.remotePort) ? `${rule.remoteDomain}:${rule.remotePort}` : rule.remoteDomain,
246
246
  };
247
247
  forwardingRules.push(forwardingRule);
package/src/types.ts CHANGED
@@ -2,10 +2,11 @@ import { PinggyOptions, TunnelUsageType } from "@pinggy/pinggy";
2
2
 
3
3
  // Local representation of additional forwarding
4
4
  export interface AdditionalForwarding {
5
- remoteDomain?: string;
6
- remotePort?: number;
7
5
  localDomain: string;
8
6
  localPort: number;
7
+ remoteDomain?: string;
8
+ remotePort?: number;
9
+ protocol?: 'http' | 'tcp' | 'udp' | 'tls';
9
10
  }
10
11
 
11
12
 
@@ -63,20 +64,13 @@ export interface Status {
63
64
  warnings: Warning[];
64
65
  }
65
66
 
66
- export type Forwarding = {
67
- remoteDomain?: string;
68
- remotePort: number;
69
- localDomain: string;
70
- localPort: number;
71
- };
72
-
73
67
  export type FinalConfig = (PinggyOptions & { configid: string }) & {
74
68
  tunnelType: string[];
75
69
  conf?: string;
76
70
  saveconf?: string;
77
71
  serve?: string;
78
72
  remoteManagement?: string;
79
- additionalForwarding?: Forwarding[];
73
+ additionalForwarding?: AdditionalForwarding[];
80
74
  manage?: string;
81
75
  version?: boolean;
82
76
  NoTUI?: boolean;