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.
- package/dist/index.cjs +380 -55
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +381 -56
- package/package.json +1 -1
- package/src/cli/buildConfig.ts +119 -4
- package/src/tui/blessed/TunnelTui.ts +47 -5
- package/src/tui/blessed/components/DisplayUpdaters.ts +80 -9
- package/src/tui/blessed/components/KeyBindings.ts +124 -22
- package/src/tui/blessed/components/Modals.ts +87 -1
- package/src/tui/blessed/config.ts +53 -0
- package/src/tui/blessed/headerFetcher.ts +9 -2
- package/src/tui/blessed/webDebuggerConnection.ts +41 -13
- package/src/tui/ink/utils/utils.ts +1 -1
- package/src/tunnel_manager/TunnelManager.ts +3 -3
- package/src/types.ts +4 -10
|
@@ -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
|
-
|
|
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:
|
|
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.
|
|
39
|
-
Res: parsed.
|
|
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
|
-
|
|
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
|
-
|
|
85
|
+
upsertPair(key, merged);
|
|
66
86
|
}
|
|
67
87
|
|
|
68
|
-
// Notify listener with
|
|
69
|
-
|
|
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 "
|
|
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,
|
|
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
|
|
244
|
-
address
|
|
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?:
|
|
73
|
+
additionalForwarding?: AdditionalForwarding[];
|
|
80
74
|
manage?: string;
|
|
81
75
|
version?: boolean;
|
|
82
76
|
NoTUI?: boolean;
|