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
package/dist/index.cjs
CHANGED
|
@@ -402,7 +402,7 @@ var TunnelManager = class _TunnelManager {
|
|
|
402
402
|
for (const rule of additionalForwarding) {
|
|
403
403
|
if (rule && rule.localDomain && rule.localPort && rule.remoteDomain && isValidPort(rule.localPort)) {
|
|
404
404
|
const forwardingRule = {
|
|
405
|
-
type:
|
|
405
|
+
type: rule.protocol,
|
|
406
406
|
// In Future we can make this dynamic based on user input
|
|
407
407
|
address: `${rule.localDomain}:${rule.localPort}`,
|
|
408
408
|
listenAddress: rule.remotePort && isValidPort(rule.remotePort) ? `${rule.remoteDomain}:${rule.remotePort}` : rule.remoteDomain
|
|
@@ -1580,7 +1580,8 @@ function ipv6SafeSplitColon(s) {
|
|
|
1580
1580
|
result.push(buf);
|
|
1581
1581
|
return result;
|
|
1582
1582
|
}
|
|
1583
|
-
|
|
1583
|
+
var VALID_PROTOCOLS = ["http", "tcp", "udp", "tls"];
|
|
1584
|
+
function parseDefaultForwarding(forwarding) {
|
|
1584
1585
|
const parts = ipv6SafeSplitColon(forwarding);
|
|
1585
1586
|
if (parts.length === 3) {
|
|
1586
1587
|
const remotePort = parseInt(parts[0], 10);
|
|
@@ -1597,6 +1598,83 @@ function parseForwarding(forwarding) {
|
|
|
1597
1598
|
}
|
|
1598
1599
|
return new Error("forwarding address incorrect");
|
|
1599
1600
|
}
|
|
1601
|
+
function parseAdditionalForwarding(forwarding) {
|
|
1602
|
+
const toPort = (v) => {
|
|
1603
|
+
const n = parseInt(v, 10);
|
|
1604
|
+
return Number.isNaN(n) ? null : n;
|
|
1605
|
+
};
|
|
1606
|
+
const validateDomain = (d) => d && domainRegex.test(d) ? d : null;
|
|
1607
|
+
let protocol = "http";
|
|
1608
|
+
let remoteDomainRaw;
|
|
1609
|
+
const protocolsRequiringDomainPort = ["tcp", "udp"];
|
|
1610
|
+
const lowForwarding = forwarding.toLowerCase();
|
|
1611
|
+
let remaining = forwarding;
|
|
1612
|
+
for (const p of VALID_PROTOCOLS) {
|
|
1613
|
+
if (lowForwarding.startsWith(p + "//")) {
|
|
1614
|
+
protocol = p;
|
|
1615
|
+
remaining = forwarding.slice(p.length + 2);
|
|
1616
|
+
break;
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
if (protocol === "http" && remaining === forwarding) {
|
|
1620
|
+
const parts2 = ipv6SafeSplitColon(remaining);
|
|
1621
|
+
if (parts2.length !== 4) {
|
|
1622
|
+
return new Error(
|
|
1623
|
+
"forwarding must be in format: domain:remotePort:localDomain:localPort"
|
|
1624
|
+
);
|
|
1625
|
+
}
|
|
1626
|
+
const remoteDomain = validateDomain(removeIPv6Brackets(parts2[0]));
|
|
1627
|
+
const localDomain2 = removeIPv6Brackets(parts2[2] || "localhost");
|
|
1628
|
+
const localPort2 = toPort(parts2[3]);
|
|
1629
|
+
if (!remoteDomain) {
|
|
1630
|
+
return new Error("forwarding address incorrect: invalid domain");
|
|
1631
|
+
}
|
|
1632
|
+
if (localPort2 === null || !isValidPort(localPort2)) {
|
|
1633
|
+
return new Error("forwarding address incorrect: invalid local port");
|
|
1634
|
+
}
|
|
1635
|
+
return {
|
|
1636
|
+
protocol: "http",
|
|
1637
|
+
remoteDomain,
|
|
1638
|
+
remotePort: 0,
|
|
1639
|
+
localDomain: localDomain2,
|
|
1640
|
+
localPort: localPort2
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1643
|
+
const domainPortMatch = remaining.match(/^([^:]+)\/(\d+):(.+)$/);
|
|
1644
|
+
if (!domainPortMatch) {
|
|
1645
|
+
return new Error(`forwarding must be in format: ${protocol}//domain/remotePort:localDomain:localPort`);
|
|
1646
|
+
}
|
|
1647
|
+
remoteDomainRaw = removeIPv6Brackets(domainPortMatch[1]);
|
|
1648
|
+
const remotePortNum = toPort(domainPortMatch[2]);
|
|
1649
|
+
const restParts = domainPortMatch[3];
|
|
1650
|
+
if (!remoteDomainRaw || !domainRegex.test(remoteDomainRaw)) {
|
|
1651
|
+
return new Error("forwarding address incorrect: invalid domain or remote port");
|
|
1652
|
+
}
|
|
1653
|
+
if (!remoteDomainRaw || remotePortNum === null || !isValidPort(remotePortNum)) {
|
|
1654
|
+
return new Error(`${protocol} forwarding: invalid domain or port in format ${protocol}//domain/remotePort`);
|
|
1655
|
+
}
|
|
1656
|
+
const parts = ipv6SafeSplitColon(restParts);
|
|
1657
|
+
if (parts.length !== 3) {
|
|
1658
|
+
return new Error(`forwarding format incorrect: expected ${protocol}//domain/remotePort:placeholder:localDomain:localPort`);
|
|
1659
|
+
}
|
|
1660
|
+
const localDomain = removeIPv6Brackets(parts[1] || "localhost");
|
|
1661
|
+
const localPort = toPort(parts[2]);
|
|
1662
|
+
if (localPort === null || !isValidPort(localPort)) {
|
|
1663
|
+
return new Error("forwarding address incorrect: invalid local port");
|
|
1664
|
+
}
|
|
1665
|
+
if (protocolsRequiringDomainPort.includes(protocol)) {
|
|
1666
|
+
if (!remoteDomainRaw || !remotePortNum) {
|
|
1667
|
+
return new Error(`${protocol} forwarding requires domain and port in format: ${protocol}//domain/remotePort:localDomain:localPort`);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
return {
|
|
1671
|
+
protocol,
|
|
1672
|
+
remoteDomain: remoteDomainRaw,
|
|
1673
|
+
remotePort: remotePortNum,
|
|
1674
|
+
localDomain,
|
|
1675
|
+
localPort
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1600
1678
|
function parseReverseTunnelAddr(finalConfig, values) {
|
|
1601
1679
|
const reverseTunnel = values.R;
|
|
1602
1680
|
if ((!Array.isArray(reverseTunnel) || reverseTunnel.length === 0) && !values.localport && !finalConfig.forwarding) {
|
|
@@ -1605,7 +1683,7 @@ function parseReverseTunnelAddr(finalConfig, values) {
|
|
|
1605
1683
|
if (!Array.isArray(reverseTunnel) || reverseTunnel.length === 0) {
|
|
1606
1684
|
return null;
|
|
1607
1685
|
}
|
|
1608
|
-
const forwarding =
|
|
1686
|
+
const forwarding = parseDefaultForwarding(reverseTunnel[0]);
|
|
1609
1687
|
if (forwarding instanceof Error) {
|
|
1610
1688
|
return forwarding;
|
|
1611
1689
|
}
|
|
@@ -1613,7 +1691,7 @@ function parseReverseTunnelAddr(finalConfig, values) {
|
|
|
1613
1691
|
if (reverseTunnel.length > 1) {
|
|
1614
1692
|
finalConfig.additionalForwarding = [];
|
|
1615
1693
|
for (const t of reverseTunnel.slice(1)) {
|
|
1616
|
-
const f =
|
|
1694
|
+
const f = parseAdditionalForwarding(t);
|
|
1617
1695
|
if (f instanceof Error) {
|
|
1618
1696
|
return f;
|
|
1619
1697
|
}
|
|
@@ -2563,11 +2641,47 @@ async function createQrCodes(urls) {
|
|
|
2563
2641
|
|
|
2564
2642
|
// src/tui/blessed/webDebuggerConnection.ts
|
|
2565
2643
|
var import_ws2 = __toESM(require("ws"), 1);
|
|
2644
|
+
|
|
2645
|
+
// src/tui/blessed/config.ts
|
|
2646
|
+
var defaultTuiConfig = {
|
|
2647
|
+
maxRequestPairs: 100,
|
|
2648
|
+
visibleRequestCount: 10,
|
|
2649
|
+
viewportScrollMargin: 2,
|
|
2650
|
+
inactivityHttpSelectorTimeoutMs: 1e4
|
|
2651
|
+
};
|
|
2652
|
+
function getTuiConfig() {
|
|
2653
|
+
return {
|
|
2654
|
+
maxRequestPairs: defaultTuiConfig.maxRequestPairs,
|
|
2655
|
+
visibleRequestCount: defaultTuiConfig.visibleRequestCount,
|
|
2656
|
+
viewportScrollMargin: defaultTuiConfig.viewportScrollMargin,
|
|
2657
|
+
inactivityHttpSelectorTimeoutMs: defaultTuiConfig.inactivityHttpSelectorTimeoutMs
|
|
2658
|
+
};
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
// src/tui/blessed/webDebuggerConnection.ts
|
|
2566
2662
|
function createWebDebuggerConnection(webDebuggerUrl, onUpdate) {
|
|
2567
2663
|
const pairs = /* @__PURE__ */ new Map();
|
|
2664
|
+
const pairKeys = [];
|
|
2568
2665
|
let socket = null;
|
|
2569
2666
|
let reconnectTimeout = null;
|
|
2570
2667
|
let isStopped = false;
|
|
2668
|
+
const config = getTuiConfig();
|
|
2669
|
+
const maxPairs = config.maxRequestPairs;
|
|
2670
|
+
const trimPairs = () => {
|
|
2671
|
+
while (pairKeys.length > maxPairs) {
|
|
2672
|
+
const oldestKey = pairKeys.shift();
|
|
2673
|
+
if (oldestKey !== void 0) {
|
|
2674
|
+
pairs.delete(oldestKey);
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
};
|
|
2678
|
+
const upsertPair = (key, pair) => {
|
|
2679
|
+
if (!pairs.has(key)) {
|
|
2680
|
+
pairKeys.push(key);
|
|
2681
|
+
}
|
|
2682
|
+
pairs.set(key, pair);
|
|
2683
|
+
trimPairs();
|
|
2684
|
+
};
|
|
2571
2685
|
const connect = () => {
|
|
2572
2686
|
const ws = new import_ws2.default(`ws://${webDebuggerUrl}/introspec/websocket`);
|
|
2573
2687
|
socket = ws;
|
|
@@ -2579,34 +2693,36 @@ function createWebDebuggerConnection(webDebuggerUrl, onUpdate) {
|
|
|
2579
2693
|
const raw = data.toString();
|
|
2580
2694
|
const parsed = JSON.parse(raw);
|
|
2581
2695
|
const msg = {
|
|
2582
|
-
Req: parsed.
|
|
2583
|
-
Res: parsed.
|
|
2696
|
+
Req: parsed.req,
|
|
2697
|
+
Res: parsed.res
|
|
2584
2698
|
};
|
|
2585
2699
|
if (msg.Req) {
|
|
2586
2700
|
const { key } = msg.Req;
|
|
2587
2701
|
const existing = pairs.get(key);
|
|
2588
2702
|
const merged = {
|
|
2589
2703
|
request: msg.Req,
|
|
2590
|
-
response: existing?.response
|
|
2591
|
-
reqHeaders: existing?.reqHeaders ?? {},
|
|
2592
|
-
resHeaders: existing?.resHeaders ?? {},
|
|
2593
|
-
headersLoaded: existing?.headersLoaded ?? false
|
|
2704
|
+
response: existing?.response
|
|
2594
2705
|
};
|
|
2595
|
-
|
|
2706
|
+
upsertPair(key, merged);
|
|
2596
2707
|
}
|
|
2597
2708
|
if (msg.Res) {
|
|
2598
2709
|
const { key } = msg.Res;
|
|
2599
2710
|
const existing = pairs.get(key);
|
|
2600
2711
|
const merged = {
|
|
2601
2712
|
request: existing?.request ?? {},
|
|
2602
|
-
response: msg.Res
|
|
2603
|
-
reqHeaders: existing?.reqHeaders ?? {},
|
|
2604
|
-
resHeaders: existing?.resHeaders ?? {},
|
|
2605
|
-
headersLoaded: existing?.headersLoaded ?? false
|
|
2713
|
+
response: msg.Res
|
|
2606
2714
|
};
|
|
2607
|
-
|
|
2715
|
+
upsertPair(key, merged);
|
|
2716
|
+
}
|
|
2717
|
+
const reversedPairs = [];
|
|
2718
|
+
for (let i = pairKeys.length - 1; i >= 0; i--) {
|
|
2719
|
+
const key = pairKeys[i];
|
|
2720
|
+
const pair = pairs.get(key);
|
|
2721
|
+
if (pair) {
|
|
2722
|
+
reversedPairs.push(pair);
|
|
2723
|
+
}
|
|
2608
2724
|
}
|
|
2609
|
-
onUpdate(
|
|
2725
|
+
onUpdate(reversedPairs);
|
|
2610
2726
|
} catch (err) {
|
|
2611
2727
|
logger.error("Error parsing WebSocket message:", err.message || err);
|
|
2612
2728
|
}
|
|
@@ -2878,7 +2994,7 @@ function getStatusColor(status) {
|
|
|
2878
2994
|
const statusCode = match ? parseInt(match[1], 10) : 0;
|
|
2879
2995
|
switch (true) {
|
|
2880
2996
|
case (statusCode >= 100 && statusCode < 200):
|
|
2881
|
-
return "
|
|
2997
|
+
return "yellow";
|
|
2882
2998
|
case (statusCode >= 200 && statusCode < 300):
|
|
2883
2999
|
return "green";
|
|
2884
3000
|
case (statusCode >= 300 && statusCode < 400):
|
|
@@ -2938,14 +3054,49 @@ Total Transfer: ${getBytesInt(stats.numTotalTxBytes)}`;
|
|
|
2938
3054
|
screen.render();
|
|
2939
3055
|
}
|
|
2940
3056
|
function updateRequestsDisplay(requestsBox, screen, pairs, selectedIndex) {
|
|
2941
|
-
|
|
2942
|
-
const
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
3057
|
+
const config = getTuiConfig();
|
|
3058
|
+
const { maxRequestPairs, visibleRequestCount, viewportScrollMargin } = config;
|
|
3059
|
+
if (!requestsBox) {
|
|
3060
|
+
return { adjustedSelectedIndex: selectedIndex, trimmedPairs: pairs };
|
|
3061
|
+
}
|
|
3062
|
+
let allPairs = pairs;
|
|
3063
|
+
let trimmedPairs = pairs;
|
|
3064
|
+
if (allPairs.length > maxRequestPairs) {
|
|
3065
|
+
allPairs = allPairs.slice(0, maxRequestPairs);
|
|
3066
|
+
trimmedPairs = allPairs;
|
|
3067
|
+
}
|
|
3068
|
+
const totalPairs = allPairs.length;
|
|
3069
|
+
let adjustedSelectedIndex = selectedIndex;
|
|
3070
|
+
if (adjustedSelectedIndex >= totalPairs) {
|
|
3071
|
+
adjustedSelectedIndex = -1;
|
|
3072
|
+
}
|
|
3073
|
+
let viewportStart;
|
|
3074
|
+
if (totalPairs <= visibleRequestCount) {
|
|
3075
|
+
viewportStart = 0;
|
|
3076
|
+
} else if (adjustedSelectedIndex === -1) {
|
|
3077
|
+
viewportStart = 0;
|
|
3078
|
+
} else {
|
|
3079
|
+
viewportStart = 0;
|
|
3080
|
+
if (adjustedSelectedIndex >= visibleRequestCount - viewportScrollMargin) {
|
|
3081
|
+
viewportStart = Math.min(
|
|
3082
|
+
totalPairs - visibleRequestCount,
|
|
3083
|
+
adjustedSelectedIndex - viewportScrollMargin
|
|
3084
|
+
);
|
|
3085
|
+
}
|
|
3086
|
+
if (adjustedSelectedIndex < viewportStart + viewportScrollMargin) {
|
|
3087
|
+
viewportStart = Math.max(0, adjustedSelectedIndex - viewportScrollMargin);
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
const viewportEnd = Math.min(viewportStart + visibleRequestCount, totalPairs);
|
|
3091
|
+
const visiblePairs = allPairs.slice(viewportStart, viewportEnd);
|
|
3092
|
+
let content = "{yellow-fg}HTTP Requests:{/yellow-fg}";
|
|
3093
|
+
if (viewportStart > 0) {
|
|
3094
|
+
content += ` {gray-fg}\u2191 ${viewportStart} more{/gray-fg}`;
|
|
3095
|
+
}
|
|
3096
|
+
content += "\n";
|
|
2946
3097
|
visiblePairs.forEach((pair, i) => {
|
|
2947
|
-
const globalIndex =
|
|
2948
|
-
const isSelected =
|
|
3098
|
+
const globalIndex = viewportStart + i;
|
|
3099
|
+
const isSelected = adjustedSelectedIndex !== -1 && adjustedSelectedIndex === globalIndex;
|
|
2949
3100
|
const prefix = isSelected ? "> " : " ";
|
|
2950
3101
|
const method = pair.request?.method || "";
|
|
2951
3102
|
const uri = pair.request?.uri || "";
|
|
@@ -2962,8 +3113,14 @@ function updateRequestsDisplay(requestsBox, screen, pairs, selectedIndex) {
|
|
|
2962
3113
|
`;
|
|
2963
3114
|
}
|
|
2964
3115
|
});
|
|
3116
|
+
const itemsBelow = totalPairs - viewportEnd;
|
|
3117
|
+
if (itemsBelow > 0) {
|
|
3118
|
+
content += `{gray-fg} \u2193 ${itemsBelow} more{/gray-fg}
|
|
3119
|
+
`;
|
|
3120
|
+
}
|
|
2965
3121
|
requestsBox.setContent(content);
|
|
2966
3122
|
screen.render();
|
|
3123
|
+
return { adjustedSelectedIndex, trimmedPairs };
|
|
2967
3124
|
}
|
|
2968
3125
|
function updateQrCodeDisplay(qrCodeBox, screen, qrCodes, urls, currentQrIndex) {
|
|
2969
3126
|
if (!qrCodeBox || qrCodes.length === 0) return;
|
|
@@ -3058,11 +3215,13 @@ function showKeyBindingsModal(screen, manager) {
|
|
|
3058
3215
|
{bold}Ctrl+c{/bold} Exit
|
|
3059
3216
|
|
|
3060
3217
|
Enter/Return Open selected request
|
|
3061
|
-
Esc Return to main page
|
|
3218
|
+
Esc Return to main page (or close modals)
|
|
3062
3219
|
UP (\u2191) Scroll up the requests
|
|
3063
3220
|
Down (\u2193) Scroll down the requests
|
|
3064
3221
|
Left (\u2190) Show qr code for previous url
|
|
3065
3222
|
Right (\u2192) Show qr code for next url
|
|
3223
|
+
Home Jump to top of requests
|
|
3224
|
+
End Jump to bottom of requests
|
|
3066
3225
|
Ctrl+c Force Exit
|
|
3067
3226
|
|
|
3068
3227
|
{white-bg}{black-fg}Press ESC to close{/black-fg}{/white-bg}`;
|
|
@@ -3127,38 +3286,117 @@ function closeDisconnectModal(screen, manager) {
|
|
|
3127
3286
|
manager.inDisconnectView = false;
|
|
3128
3287
|
screen.render();
|
|
3129
3288
|
}
|
|
3289
|
+
function showLoadingModal(screen, modalManager, message = "Loading...") {
|
|
3290
|
+
if (modalManager.loadingView) return;
|
|
3291
|
+
modalManager.loadingBox = import_blessed2.default.box({
|
|
3292
|
+
parent: screen,
|
|
3293
|
+
top: "center",
|
|
3294
|
+
left: "center",
|
|
3295
|
+
width: "60%",
|
|
3296
|
+
height: 8,
|
|
3297
|
+
border: { type: "line" },
|
|
3298
|
+
style: {
|
|
3299
|
+
border: { fg: "yellow" }
|
|
3300
|
+
},
|
|
3301
|
+
tags: true,
|
|
3302
|
+
content: `{center}{yellow-fg}{bold}${message}{/bold}{/yellow-fg}
|
|
3303
|
+
|
|
3304
|
+
{gray-fg}Press ESC to cancel{/gray-fg}{/center}`,
|
|
3305
|
+
valign: "middle"
|
|
3306
|
+
});
|
|
3307
|
+
modalManager.loadingView = true;
|
|
3308
|
+
screen.render();
|
|
3309
|
+
}
|
|
3310
|
+
function closeLoadingModal(screen, modalManager) {
|
|
3311
|
+
if (!modalManager.loadingView || !modalManager.loadingBox) return;
|
|
3312
|
+
modalManager.loadingBox.destroy();
|
|
3313
|
+
modalManager.loadingBox = null;
|
|
3314
|
+
modalManager.loadingView = false;
|
|
3315
|
+
screen.render();
|
|
3316
|
+
}
|
|
3317
|
+
function showErrorModal(screen, modalManager, title = "Error", message) {
|
|
3318
|
+
if (modalManager.loadingBox) {
|
|
3319
|
+
modalManager.loadingBox.destroy();
|
|
3320
|
+
modalManager.loadingBox = null;
|
|
3321
|
+
}
|
|
3322
|
+
modalManager.loadingBox = import_blessed2.default.box({
|
|
3323
|
+
parent: screen,
|
|
3324
|
+
top: "center",
|
|
3325
|
+
left: "center",
|
|
3326
|
+
width: "60%",
|
|
3327
|
+
height: 9,
|
|
3328
|
+
border: { type: "line" },
|
|
3329
|
+
style: {
|
|
3330
|
+
border: { fg: "red" }
|
|
3331
|
+
},
|
|
3332
|
+
tags: true,
|
|
3333
|
+
content: `{center}{red-fg}{bold}${title}{/bold}{/red-fg}
|
|
3334
|
+
|
|
3335
|
+
{white-fg}${message}{/white-fg}
|
|
3336
|
+
|
|
3337
|
+
{gray-fg}Press ESC to close{/gray-fg}{/center}`,
|
|
3338
|
+
valign: "middle"
|
|
3339
|
+
});
|
|
3340
|
+
modalManager.loadingView = true;
|
|
3341
|
+
screen.render();
|
|
3342
|
+
}
|
|
3130
3343
|
|
|
3131
3344
|
// src/tui/blessed/headerFetcher.ts
|
|
3132
|
-
async function fetchReqResHeaders(baseUrl, key) {
|
|
3345
|
+
async function fetchReqResHeaders(baseUrl, key, signal) {
|
|
3133
3346
|
if (!baseUrl) {
|
|
3134
3347
|
return { req: "", res: "" };
|
|
3135
3348
|
}
|
|
3136
3349
|
try {
|
|
3137
3350
|
const [reqRes, resRes] = await Promise.all([
|
|
3138
3351
|
fetch(`http://${baseUrl}/introspec/getrawrequestheader`, {
|
|
3139
|
-
headers: { "X-Introspec-Key": key.toString() }
|
|
3352
|
+
headers: { "X-Introspec-Key": key.toString() },
|
|
3353
|
+
signal
|
|
3140
3354
|
}),
|
|
3141
3355
|
fetch(`http://${baseUrl}/introspec/getrawresponseheader`, {
|
|
3142
|
-
headers: { "X-Introspec-Key": key.toString() }
|
|
3356
|
+
headers: { "X-Introspec-Key": key.toString() },
|
|
3357
|
+
signal
|
|
3143
3358
|
})
|
|
3144
3359
|
]);
|
|
3145
3360
|
const [req, res] = await Promise.all([reqRes.text(), resRes.text()]);
|
|
3146
3361
|
return { req, res };
|
|
3147
3362
|
} catch (err) {
|
|
3363
|
+
if (err?.name === "AbortError") {
|
|
3364
|
+
throw err;
|
|
3365
|
+
}
|
|
3148
3366
|
logger.error("Error fetching headers:", err.message || err);
|
|
3149
|
-
|
|
3367
|
+
throw err;
|
|
3150
3368
|
}
|
|
3151
3369
|
}
|
|
3152
3370
|
|
|
3153
3371
|
// src/tui/blessed/components/KeyBindings.ts
|
|
3154
|
-
function setupKeyBindings(screen, modalManager, state, callbacks, tunnelConfig
|
|
3372
|
+
function setupKeyBindings(screen, modalManager, state, callbacks, tunnelConfig) {
|
|
3373
|
+
let inactivityTimeout = null;
|
|
3374
|
+
const { inactivityHttpSelectorTimeoutMs } = getTuiConfig();
|
|
3375
|
+
const INACTIVITY_TIMEOUT_MS = inactivityHttpSelectorTimeoutMs;
|
|
3376
|
+
const resetInactivityTimer = () => {
|
|
3377
|
+
if (inactivityTimeout) {
|
|
3378
|
+
clearTimeout(inactivityTimeout);
|
|
3379
|
+
}
|
|
3380
|
+
if (state.selectedIndex !== -1) {
|
|
3381
|
+
inactivityTimeout = setTimeout(() => {
|
|
3382
|
+
callbacks.onSelectedIndexChange(-1, null);
|
|
3383
|
+
callbacks.updateRequestsDisplay();
|
|
3384
|
+
}, INACTIVITY_TIMEOUT_MS);
|
|
3385
|
+
}
|
|
3386
|
+
};
|
|
3155
3387
|
screen.key(["C-c"], () => {
|
|
3156
|
-
const manager = TunnelManager.getInstance();
|
|
3157
|
-
manager.stopTunnel(tunnelInstance?.tunnelid || "");
|
|
3158
3388
|
callbacks.onDestroy();
|
|
3159
3389
|
process.exit(0);
|
|
3160
3390
|
});
|
|
3161
3391
|
screen.key(["escape"], () => {
|
|
3392
|
+
if (modalManager.loadingView) {
|
|
3393
|
+
if (modalManager.fetchAbortController) {
|
|
3394
|
+
modalManager.fetchAbortController.abort();
|
|
3395
|
+
modalManager.fetchAbortController = null;
|
|
3396
|
+
}
|
|
3397
|
+
closeLoadingModal(screen, modalManager);
|
|
3398
|
+
return;
|
|
3399
|
+
}
|
|
3162
3400
|
if (modalManager.inDetailView) {
|
|
3163
3401
|
closeDetailModal(screen, modalManager);
|
|
3164
3402
|
return;
|
|
@@ -3167,40 +3405,97 @@ function setupKeyBindings(screen, modalManager, state, callbacks, tunnelConfig,
|
|
|
3167
3405
|
closeKeyBindingsModal(screen, modalManager);
|
|
3168
3406
|
return;
|
|
3169
3407
|
}
|
|
3408
|
+
if (state.selectedIndex !== -1) {
|
|
3409
|
+
if (inactivityTimeout) {
|
|
3410
|
+
clearTimeout(inactivityTimeout);
|
|
3411
|
+
inactivityTimeout = null;
|
|
3412
|
+
}
|
|
3413
|
+
callbacks.onSelectedIndexChange(-1, null);
|
|
3414
|
+
callbacks.updateRequestsDisplay();
|
|
3415
|
+
}
|
|
3170
3416
|
});
|
|
3171
3417
|
screen.key(["up"], () => {
|
|
3172
|
-
if (modalManager.inDetailView || modalManager.keyBindingView) return;
|
|
3173
|
-
|
|
3174
|
-
|
|
3418
|
+
if (modalManager.inDetailView || modalManager.keyBindingView || modalManager.loadingView) return;
|
|
3419
|
+
resetInactivityTimer();
|
|
3420
|
+
if (state.selectedIndex === -1) {
|
|
3421
|
+
const requestKey = state.pairs[0]?.request?.key ?? null;
|
|
3422
|
+
callbacks.onSelectedIndexChange(0, requestKey);
|
|
3423
|
+
callbacks.updateRequestsDisplay();
|
|
3424
|
+
resetInactivityTimer();
|
|
3425
|
+
} else if (state.selectedIndex > 0) {
|
|
3426
|
+
const newIndex = state.selectedIndex - 1;
|
|
3427
|
+
const requestKey = state.pairs[newIndex]?.request?.key ?? null;
|
|
3428
|
+
callbacks.onSelectedIndexChange(newIndex, requestKey);
|
|
3175
3429
|
callbacks.updateRequestsDisplay();
|
|
3176
3430
|
}
|
|
3177
3431
|
});
|
|
3178
3432
|
screen.key(["down"], () => {
|
|
3179
|
-
if (modalManager.inDetailView || modalManager.keyBindingView) return;
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3433
|
+
if (modalManager.inDetailView || modalManager.keyBindingView || modalManager.loadingView) return;
|
|
3434
|
+
resetInactivityTimer();
|
|
3435
|
+
const config = getTuiConfig();
|
|
3436
|
+
const limitedLength = Math.min(state.pairs.length, config.maxRequestPairs);
|
|
3437
|
+
if (state.selectedIndex === -1) {
|
|
3438
|
+
if (limitedLength > 0) {
|
|
3439
|
+
const requestKey = state.pairs[0]?.request?.key ?? null;
|
|
3440
|
+
callbacks.onSelectedIndexChange(0, requestKey);
|
|
3441
|
+
callbacks.updateRequestsDisplay();
|
|
3442
|
+
resetInactivityTimer();
|
|
3443
|
+
}
|
|
3444
|
+
} else if (state.selectedIndex < limitedLength - 1) {
|
|
3445
|
+
const newIndex = state.selectedIndex + 1;
|
|
3446
|
+
const requestKey = state.pairs[newIndex]?.request?.key ?? null;
|
|
3447
|
+
callbacks.onSelectedIndexChange(newIndex, requestKey);
|
|
3448
|
+
callbacks.updateRequestsDisplay();
|
|
3449
|
+
}
|
|
3450
|
+
});
|
|
3451
|
+
screen.key(["end"], () => {
|
|
3452
|
+
if (modalManager.inDetailView || modalManager.keyBindingView || modalManager.loadingView) return;
|
|
3453
|
+
resetInactivityTimer();
|
|
3454
|
+
const config = getTuiConfig();
|
|
3455
|
+
const limitedLength = Math.min(state.pairs.length, config.maxRequestPairs);
|
|
3456
|
+
const lastIndex = Math.max(0, limitedLength - 1);
|
|
3457
|
+
if (state.selectedIndex !== lastIndex) {
|
|
3458
|
+
const requestKey = state.pairs[lastIndex]?.request?.key ?? null;
|
|
3459
|
+
callbacks.onSelectedIndexChange(lastIndex, requestKey);
|
|
3183
3460
|
callbacks.updateRequestsDisplay();
|
|
3184
3461
|
}
|
|
3185
3462
|
});
|
|
3186
3463
|
screen.key(["enter"], async () => {
|
|
3187
|
-
if (modalManager.inDetailView || modalManager.keyBindingView) return;
|
|
3188
|
-
|
|
3189
|
-
|
|
3464
|
+
if (modalManager.inDetailView || modalManager.keyBindingView || modalManager.loadingView) return;
|
|
3465
|
+
if (state.selectedIndex === -1) return;
|
|
3466
|
+
resetInactivityTimer();
|
|
3467
|
+
const pair = state.pairs[state.selectedIndex];
|
|
3190
3468
|
if (pair?.request?.key !== void 0 && pair?.request?.key !== null) {
|
|
3469
|
+
const abortController = new AbortController();
|
|
3470
|
+
modalManager.fetchAbortController = abortController;
|
|
3471
|
+
showLoadingModal(screen, modalManager, "Fetching request details...");
|
|
3191
3472
|
try {
|
|
3192
3473
|
const headers = await fetchReqResHeaders(
|
|
3193
3474
|
tunnelConfig?.webDebugger || "",
|
|
3194
|
-
pair.request.key
|
|
3475
|
+
pair.request.key,
|
|
3476
|
+
abortController.signal
|
|
3195
3477
|
);
|
|
3478
|
+
if (abortController.signal.aborted) {
|
|
3479
|
+
return;
|
|
3480
|
+
}
|
|
3481
|
+
closeLoadingModal(screen, modalManager);
|
|
3482
|
+
modalManager.fetchAbortController = null;
|
|
3196
3483
|
showDetailModal(screen, modalManager, headers.req, headers.res);
|
|
3197
3484
|
} catch (err) {
|
|
3485
|
+
if (err?.name === "AbortError" || abortController.signal.aborted) {
|
|
3486
|
+
logger.info("Fetch request cancelled by user");
|
|
3487
|
+
return;
|
|
3488
|
+
}
|
|
3489
|
+
closeLoadingModal(screen, modalManager);
|
|
3490
|
+
modalManager.fetchAbortController = null;
|
|
3491
|
+
const errorMessage = err?.message || String(err) || "Unknown error occurred";
|
|
3198
3492
|
logger.error("Fetch error:", err);
|
|
3493
|
+
showErrorModal(screen, modalManager, "Failed to fetch request details", errorMessage);
|
|
3199
3494
|
}
|
|
3200
3495
|
}
|
|
3201
3496
|
});
|
|
3202
3497
|
screen.key(["h"], () => {
|
|
3203
|
-
if (modalManager.inDetailView) return;
|
|
3498
|
+
if (modalManager.inDetailView || modalManager.loadingView) return;
|
|
3204
3499
|
if (modalManager.keyBindingView) {
|
|
3205
3500
|
closeKeyBindingsModal(screen, modalManager);
|
|
3206
3501
|
} else {
|
|
@@ -3208,7 +3503,7 @@ function setupKeyBindings(screen, modalManager, state, callbacks, tunnelConfig,
|
|
|
3208
3503
|
}
|
|
3209
3504
|
});
|
|
3210
3505
|
screen.key(["c"], async () => {
|
|
3211
|
-
if (modalManager.inDetailView || modalManager.keyBindingView) return;
|
|
3506
|
+
if (modalManager.inDetailView || modalManager.keyBindingView || modalManager.loadingView) return;
|
|
3212
3507
|
if (state.urls.length > 0) {
|
|
3213
3508
|
try {
|
|
3214
3509
|
const clipboardy = await import("clipboardy");
|
|
@@ -3219,7 +3514,7 @@ function setupKeyBindings(screen, modalManager, state, callbacks, tunnelConfig,
|
|
|
3219
3514
|
}
|
|
3220
3515
|
});
|
|
3221
3516
|
screen.key(["left"], () => {
|
|
3222
|
-
if (modalManager.inDetailView || modalManager.keyBindingView) return;
|
|
3517
|
+
if (modalManager.inDetailView || modalManager.keyBindingView || modalManager.loadingView) return;
|
|
3223
3518
|
if (state.currentQrIndex > 0) {
|
|
3224
3519
|
callbacks.onQrIndexChange(state.currentQrIndex - 1);
|
|
3225
3520
|
callbacks.updateUrlsDisplay();
|
|
@@ -3227,7 +3522,7 @@ function setupKeyBindings(screen, modalManager, state, callbacks, tunnelConfig,
|
|
|
3227
3522
|
}
|
|
3228
3523
|
});
|
|
3229
3524
|
screen.key(["right"], () => {
|
|
3230
|
-
if (modalManager.inDetailView || modalManager.keyBindingView) return;
|
|
3525
|
+
if (modalManager.inDetailView || modalManager.keyBindingView || modalManager.loadingView) return;
|
|
3231
3526
|
if (state.currentQrIndex < state.urls.length - 1) {
|
|
3232
3527
|
callbacks.onQrIndexChange(state.currentQrIndex + 1);
|
|
3233
3528
|
callbacks.updateUrlsDisplay();
|
|
@@ -3241,7 +3536,10 @@ var TunnelTui = class {
|
|
|
3241
3536
|
constructor(props) {
|
|
3242
3537
|
// State
|
|
3243
3538
|
this.currentQrIndex = 0;
|
|
3244
|
-
this.selectedIndex =
|
|
3539
|
+
this.selectedIndex = -1;
|
|
3540
|
+
// -1 means no selection
|
|
3541
|
+
this.selectedRequestKey = null;
|
|
3542
|
+
// Track selected request by key
|
|
3245
3543
|
this.qrCodes = [];
|
|
3246
3544
|
this.stats = {
|
|
3247
3545
|
elapsedTime: 0,
|
|
@@ -3251,7 +3549,7 @@ var TunnelTui = class {
|
|
|
3251
3549
|
numTotalResBytes: 0,
|
|
3252
3550
|
numTotalTxBytes: 0
|
|
3253
3551
|
};
|
|
3254
|
-
this.pairs =
|
|
3552
|
+
this.pairs = [];
|
|
3255
3553
|
this.webDebuggerConnection = null;
|
|
3256
3554
|
this.modalManager = {
|
|
3257
3555
|
detailModal: null,
|
|
@@ -3259,7 +3557,10 @@ var TunnelTui = class {
|
|
|
3259
3557
|
disconnectModal: null,
|
|
3260
3558
|
inDetailView: false,
|
|
3261
3559
|
keyBindingView: false,
|
|
3262
|
-
inDisconnectView: false
|
|
3560
|
+
inDisconnectView: false,
|
|
3561
|
+
loadingBox: null,
|
|
3562
|
+
loadingView: false,
|
|
3563
|
+
fetchAbortController: null
|
|
3263
3564
|
};
|
|
3264
3565
|
this.exitPromiseResolve = null;
|
|
3265
3566
|
this.urls = props.urls;
|
|
@@ -3289,12 +3590,26 @@ var TunnelTui = class {
|
|
|
3289
3590
|
this.updateStatsDisplay();
|
|
3290
3591
|
};
|
|
3291
3592
|
}
|
|
3593
|
+
clearSelection() {
|
|
3594
|
+
this.selectedIndex = -1;
|
|
3595
|
+
this.selectedRequestKey = null;
|
|
3596
|
+
}
|
|
3292
3597
|
setupWebDebugger() {
|
|
3293
3598
|
if (this.tunnelConfig?.webDebugger) {
|
|
3294
3599
|
this.webDebuggerConnection = createWebDebuggerConnection(
|
|
3295
3600
|
this.tunnelConfig.webDebugger,
|
|
3296
3601
|
(pairs) => {
|
|
3297
3602
|
this.pairs = pairs;
|
|
3603
|
+
if (this.selectedRequestKey !== null) {
|
|
3604
|
+
const newIndex = pairs.findIndex(
|
|
3605
|
+
(pair) => pair.request?.key === this.selectedRequestKey
|
|
3606
|
+
);
|
|
3607
|
+
if (newIndex !== -1) {
|
|
3608
|
+
this.selectedIndex = newIndex;
|
|
3609
|
+
} else {
|
|
3610
|
+
this.clearSelection();
|
|
3611
|
+
}
|
|
3612
|
+
}
|
|
3298
3613
|
this.updateRequestsDisplay();
|
|
3299
3614
|
}
|
|
3300
3615
|
);
|
|
@@ -3357,12 +3672,22 @@ var TunnelTui = class {
|
|
|
3357
3672
|
);
|
|
3358
3673
|
}
|
|
3359
3674
|
updateRequestsDisplay() {
|
|
3360
|
-
updateRequestsDisplay(
|
|
3675
|
+
const result = updateRequestsDisplay(
|
|
3361
3676
|
this.uiElements?.requestsBox,
|
|
3362
3677
|
this.screen,
|
|
3363
3678
|
this.pairs,
|
|
3364
3679
|
this.selectedIndex
|
|
3365
3680
|
);
|
|
3681
|
+
if (result.adjustedSelectedIndex !== this.selectedIndex) {
|
|
3682
|
+
if (result.adjustedSelectedIndex === -1) {
|
|
3683
|
+
this.clearSelection();
|
|
3684
|
+
} else {
|
|
3685
|
+
this.selectedIndex = result.adjustedSelectedIndex;
|
|
3686
|
+
}
|
|
3687
|
+
}
|
|
3688
|
+
if (result.trimmedPairs !== this.pairs) {
|
|
3689
|
+
this.pairs = result.trimmedPairs;
|
|
3690
|
+
}
|
|
3366
3691
|
}
|
|
3367
3692
|
updateQrCodeDisplay() {
|
|
3368
3693
|
updateQrCodeDisplay(
|
|
@@ -3399,8 +3724,9 @@ var TunnelTui = class {
|
|
|
3399
3724
|
onQrIndexChange: (index) => {
|
|
3400
3725
|
self.currentQrIndex = index;
|
|
3401
3726
|
},
|
|
3402
|
-
onSelectedIndexChange: (index) => {
|
|
3727
|
+
onSelectedIndexChange: (index, requestKey) => {
|
|
3403
3728
|
self.selectedIndex = index;
|
|
3729
|
+
self.selectedRequestKey = requestKey;
|
|
3404
3730
|
},
|
|
3405
3731
|
onDestroy: () => self.destroy(),
|
|
3406
3732
|
updateUrlsDisplay: () => self.updateUrlsDisplay(),
|
|
@@ -3412,8 +3738,7 @@ var TunnelTui = class {
|
|
|
3412
3738
|
this.modalManager,
|
|
3413
3739
|
state,
|
|
3414
3740
|
callbacks,
|
|
3415
|
-
this.tunnelConfig
|
|
3416
|
-
this.tunnelInstance
|
|
3741
|
+
this.tunnelConfig
|
|
3417
3742
|
);
|
|
3418
3743
|
}
|
|
3419
3744
|
handleResize() {
|
package/dist/index.d.cts
CHANGED
|
@@ -5,10 +5,11 @@ import { z } from 'zod';
|
|
|
5
5
|
import winston from 'winston';
|
|
6
6
|
|
|
7
7
|
interface AdditionalForwarding {
|
|
8
|
-
remoteDomain?: string;
|
|
9
|
-
remotePort?: number;
|
|
10
8
|
localDomain: string;
|
|
11
9
|
localPort: number;
|
|
10
|
+
remoteDomain?: string;
|
|
11
|
+
remotePort?: number;
|
|
12
|
+
protocol?: 'http' | 'tcp' | 'udp' | 'tls';
|
|
12
13
|
}
|
|
13
14
|
declare enum TunnelStateType {
|
|
14
15
|
New = "idle",
|