pinggy 0.4.9 → 0.5.0
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/CLAUDE.md +112 -0
- package/README.md +214 -97
- package/dist/TunnelManager-OPUMAZFX.js +11 -0
- package/dist/TunnelTui-QZEWWH2H.js +1338 -0
- package/dist/{chunk-3RTRUYNW.js → chunk-7G6SJEEA.js} +35 -7
- package/dist/chunk-BFARGPGP.js +164 -0
- package/dist/chunk-DLNUDW6G.js +1690 -0
- package/dist/chunk-FVLXFHBL.js +2157 -0
- package/dist/chunk-GBYF2H4H.js +77 -0
- package/dist/chunk-HUP6YWH6.js +269 -0
- package/dist/chunk-MT44NAXX.js +36 -0
- package/dist/chunk-UB26QJ4T.js +10 -0
- package/dist/chunk-YJQC6LQN.js +3407 -0
- package/dist/configStore-TSGRNOE3.js +42 -0
- package/dist/daemonChild-E2CORSSB.js +24 -0
- package/dist/daemonConfig-G6S46GPJ.js +9 -0
- package/dist/index.cjs +5153 -1596
- package/dist/index.d.cts +473 -13
- package/dist/index.d.ts +473 -13
- package/dist/index.js +12 -5
- package/dist/ipcClient-LZQCCNMR.js +6 -0
- package/dist/main-F4U5R4SW.js +42 -0
- package/dist/workers/file_serve_worker.cjs +70 -21
- package/dist/workers/file_serve_worker.js +15 -9
- package/eslint.config.js +27 -0
- package/package.json +8 -4
- package/dist/chunk-YFTL44B3.js +0 -2857
- package/dist/main-4WTJG54V.js +0 -2925
package/dist/main-4WTJG54V.js
DELETED
|
@@ -1,2925 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
RemoteManagementUnauthorizedError,
|
|
4
|
-
TunnelManager,
|
|
5
|
-
TunnelOperations,
|
|
6
|
-
buildRemoteManagementWsUrl,
|
|
7
|
-
closeRemoteManagement,
|
|
8
|
-
getRandomId,
|
|
9
|
-
getRemoteManagementState,
|
|
10
|
-
getVersion,
|
|
11
|
-
initiateRemoteManagement,
|
|
12
|
-
isValidPort,
|
|
13
|
-
parseRemoteManagement,
|
|
14
|
-
printer_default,
|
|
15
|
-
startRemoteManagement
|
|
16
|
-
} from "./chunk-YFTL44B3.js";
|
|
17
|
-
import {
|
|
18
|
-
configureLogger,
|
|
19
|
-
enablePackageLogging,
|
|
20
|
-
logger
|
|
21
|
-
} from "./chunk-3RTRUYNW.js";
|
|
22
|
-
|
|
23
|
-
// src/cli/options.ts
|
|
24
|
-
var cliOptions = {
|
|
25
|
-
// SSH-like options
|
|
26
|
-
R: { type: "string", multiple: true, description: "Local port. Eg. -R0:localhost:3000 will forward tunnel connections to local port 3000." },
|
|
27
|
-
L: { type: "string", multiple: true, description: "Web Debugger address. Eg. -L4300:localhost:4300 will start web debugger on port 4300." },
|
|
28
|
-
o: { type: "string", multiple: true, description: "Options", hidden: true },
|
|
29
|
-
"server-port": { type: "string", short: "p", description: "Pinggy server port. Default: 443" },
|
|
30
|
-
v4: { type: "boolean", short: "4", description: "IPv4 only", hidden: true },
|
|
31
|
-
v6: { type: "boolean", short: "6", description: "IPv6 only", hidden: true },
|
|
32
|
-
// These options appear in the ssh command, but we ignore it in CLI
|
|
33
|
-
t: { type: "boolean", description: "hidden", hidden: true },
|
|
34
|
-
T: { type: "boolean", description: "hidden", hidden: true },
|
|
35
|
-
n: { type: "boolean", description: "hidden", hidden: true },
|
|
36
|
-
N: { type: "boolean", description: "hidden", hidden: true },
|
|
37
|
-
// Better options
|
|
38
|
-
type: { type: "string", description: "Type of the connection. Eg. --type tcp" },
|
|
39
|
-
localport: { type: "string", short: "l", description: "Takes input as [protocol:][host:]port. Eg. --localport https://localhost:8000 OR -l 3000" },
|
|
40
|
-
debugger: { type: "string", short: "d", description: "Port for web debugger. Eg. --debugger 4300 OR -d 4300" },
|
|
41
|
-
token: { type: "string", description: "Token for authentication. Eg. --token TOKEN_VALUE" },
|
|
42
|
-
force: { type: "boolean", short: "f", description: "Forcefully close existing tunnels and establish a new tunnel" },
|
|
43
|
-
// Logging options (CLI overrides env)
|
|
44
|
-
loglevel: { type: "string", description: "Logging level: ERROR, INFO, DEBUG. Overrides PINGGY_LOG_LEVEL environment variable" },
|
|
45
|
-
logfile: { type: "string", description: "Path to log file. Overrides PINGGY_LOG_FILE environment variable" },
|
|
46
|
-
v: { type: "boolean", description: "Print logs to stdout for Cli. Overrides PINGGY_LOG_STDOUT environment variable" },
|
|
47
|
-
vv: { type: "boolean", description: "Enable detailed logging for the Node.js SDK and Libpinggy, including both info and debug level logs." },
|
|
48
|
-
vvv: { type: "boolean", description: "Enable all logs from Cli, SDK and internal components." },
|
|
49
|
-
"no-autoreconnect": { type: "boolean", short: "a", description: "Disable auto reconnection on failure (enabled by default)." },
|
|
50
|
-
// Save and load config (legacy file-based)
|
|
51
|
-
saveconf: { type: "string", description: "Create the configuration file based on the options provided here" },
|
|
52
|
-
conf: { type: "string", description: "Use the configuration file as base. Other options will be used to override this file" },
|
|
53
|
-
// Used by `pinggy config save` and `buildAndStartTunnel` save flow
|
|
54
|
-
save: { type: "boolean", short: "s", description: "Save the tunnel config (use with config save or -l)", hidden: true },
|
|
55
|
-
name: { type: "string", description: "Name for the tunnel config", hidden: true },
|
|
56
|
-
auto: { type: "boolean", description: "Mark tunnel config for auto-start", hidden: true },
|
|
57
|
-
// File server
|
|
58
|
-
serve: { type: "string", description: "Start a webserver to serve files from the specified path. Eg --serve /path/to/files" },
|
|
59
|
-
// Remote Control
|
|
60
|
-
"remote-management": { type: "string", description: "Enable remote management of tunnels with token. Eg. --remote-management API_KEY" },
|
|
61
|
-
manage: { type: "string", description: "Provide a server address to manage tunnels. Eg --manage dashboard.pinggy.io" },
|
|
62
|
-
noTui: { type: "boolean", description: "Disable TUI in remote management mode" },
|
|
63
|
-
notui: { type: "boolean", description: "hidden", hidden: true },
|
|
64
|
-
// Misc
|
|
65
|
-
version: { type: "boolean", description: "Print version" },
|
|
66
|
-
// Help
|
|
67
|
-
help: { type: "boolean", short: "h", description: "Show this help message" }
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
// src/cli/help.ts
|
|
71
|
-
function printHelpMessage() {
|
|
72
|
-
console.log("\nPinggy CLI Tool - Create secure tunnels to your localhost.");
|
|
73
|
-
console.log("\nUsage:");
|
|
74
|
-
console.log(" pinggy [options] -l <port>\n");
|
|
75
|
-
console.log("Options:");
|
|
76
|
-
for (const [key, value] of Object.entries(cliOptions)) {
|
|
77
|
-
if (value.hidden) continue;
|
|
78
|
-
const short = "short" in value && value.short ? `-${value.short}, ` : " ";
|
|
79
|
-
const optType = value.type === "boolean" ? "" : "<value>";
|
|
80
|
-
console.log(` ${short}--${key.padEnd(17)} ${optType.padEnd(8)} ${value.description}`);
|
|
81
|
-
}
|
|
82
|
-
console.log("\nExtended options :");
|
|
83
|
-
console.log(" x:https Enforce HTTPS only (redirect HTTP to HTTPS)");
|
|
84
|
-
console.log(" x:noreverseproxy Disable built-in reverse-proxy header injection");
|
|
85
|
-
console.log(" x:localservertls:host Connect to local HTTPS server with SNI");
|
|
86
|
-
console.log(" x:passpreflight Pass CORS preflight requests unchanged");
|
|
87
|
-
console.log(" a:Key:Val Add header");
|
|
88
|
-
console.log(" u:Key:Val Update header");
|
|
89
|
-
console.log(" r:Key Remove header");
|
|
90
|
-
console.log(" b:user:pass Basic auth");
|
|
91
|
-
console.log(" k:BEARER Bearer token");
|
|
92
|
-
console.log(" w:192.168.1.0/24 IP whitelist (CIDR)");
|
|
93
|
-
console.log("\nExamples (User-friendly):");
|
|
94
|
-
console.log(" pinggy -l 3000 # HTTP(S) tunnel to localhost port 3000");
|
|
95
|
-
console.log(" pinggy --type tcp -l 22 # TCP tunnel for SSH (port 22)");
|
|
96
|
-
console.log(" pinggy -l 8080 -d 4300 # HTTP tunnel to port 8080 with debugger running at localhost:4300");
|
|
97
|
-
console.log(" pinggy --token mytoken -l 3000 # Authenticated tunnel");
|
|
98
|
-
console.log(" pinggy x:https x:xff -l https://localhost:8443 # HTTPS-only + XFF");
|
|
99
|
-
console.log(" pinggy w:192.168.1.0/24 -l 8080 # IP whitelist restriction");
|
|
100
|
-
console.log("\nExamples (SSH-style):");
|
|
101
|
-
console.log(" pinggy -R0:localhost:3000 # Basic HTTP tunnel");
|
|
102
|
-
console.log(" pinggy --type tcp -R0:localhost:22 # TCP tunnel for SSH");
|
|
103
|
-
console.log(" pinggy -R0:localhost:8080 -L4300:localhost:4300 # HTTP tunnel with debugger");
|
|
104
|
-
console.log(" pinggy tcp@ap.example.com -R0:localhost:22 # TCP tunnel to region");
|
|
105
|
-
console.log("\nConfig Management:");
|
|
106
|
-
console.log(" pinggy config list # List saved configs");
|
|
107
|
-
console.log(" pinggy config show my-tunnel # Show config details");
|
|
108
|
-
console.log(" pinggy config save my-tunnel -l 3000 token@pro.pinggy.io # Save config");
|
|
109
|
-
console.log(" pinggy config save my-tunnel --auto -l 3000 # Save with auto-start");
|
|
110
|
-
console.log(" pinggy config update my-tunnel -l 4000 # Update saved config");
|
|
111
|
-
console.log(" pinggy config delete my-tunnel # Delete saved config");
|
|
112
|
-
console.log(" pinggy config auto my-tunnel # Enable auto-start");
|
|
113
|
-
console.log(" pinggy config noauto my-tunnel # Disable auto-start");
|
|
114
|
-
console.log("\nStart Saved Tunnels:");
|
|
115
|
-
console.log(" pinggy start my-tunnel # Start saved tunnel");
|
|
116
|
-
console.log(" pinggy start my-tunnel -l 4000 # Start with runtime overrides");
|
|
117
|
-
console.log(" pinggy start tunnela tunnelb # Start multiple tunnels");
|
|
118
|
-
console.log(" pinggy start --all # Start all auto-start tunnels\n");
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// src/utils/parseArgs.ts
|
|
122
|
-
import { parseArgs } from "util";
|
|
123
|
-
import * as os from "os";
|
|
124
|
-
function isAttachedReverseOrLocalFlag(arg) {
|
|
125
|
-
return /^-[RL].+/.test(arg);
|
|
126
|
-
}
|
|
127
|
-
function shouldMergeReverseOrLocalFragment(current, next) {
|
|
128
|
-
if (next.startsWith("-")) {
|
|
129
|
-
return false;
|
|
130
|
-
}
|
|
131
|
-
if (next.startsWith(".")) {
|
|
132
|
-
return true;
|
|
133
|
-
}
|
|
134
|
-
const body = current.slice(2);
|
|
135
|
-
if (body.endsWith(":")) {
|
|
136
|
-
return true;
|
|
137
|
-
}
|
|
138
|
-
if (body.includes("//") && !body.includes(":")) {
|
|
139
|
-
return true;
|
|
140
|
-
}
|
|
141
|
-
return false;
|
|
142
|
-
}
|
|
143
|
-
function preprocessWindowsArgs(args) {
|
|
144
|
-
if (os.platform() !== "win32") {
|
|
145
|
-
return args;
|
|
146
|
-
}
|
|
147
|
-
;
|
|
148
|
-
const out = [];
|
|
149
|
-
let i = 0;
|
|
150
|
-
while (i < args.length) {
|
|
151
|
-
const arg = args[i];
|
|
152
|
-
if (isAttachedReverseOrLocalFlag(arg)) {
|
|
153
|
-
let merged = arg;
|
|
154
|
-
while (i + 1 < args.length && shouldMergeReverseOrLocalFragment(merged, args[i + 1])) {
|
|
155
|
-
merged += args[i + 1];
|
|
156
|
-
i++;
|
|
157
|
-
}
|
|
158
|
-
out.push(merged);
|
|
159
|
-
i++;
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
out.push(arg);
|
|
163
|
-
i++;
|
|
164
|
-
}
|
|
165
|
-
return out;
|
|
166
|
-
}
|
|
167
|
-
function parseCliArgs(options, overrideArgs) {
|
|
168
|
-
const rawArgs = overrideArgs ?? process.argv.slice(2);
|
|
169
|
-
const processedArgs = preprocessWindowsArgs(rawArgs);
|
|
170
|
-
const parsed = parseArgs({
|
|
171
|
-
args: processedArgs,
|
|
172
|
-
options,
|
|
173
|
-
allowPositionals: true
|
|
174
|
-
});
|
|
175
|
-
const hasAnyArgs = parsed.positionals.length > 0 || Object.values(parsed.values).some((v) => v !== void 0 && v !== false);
|
|
176
|
-
return {
|
|
177
|
-
...parsed,
|
|
178
|
-
hasAnyArgs
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// src/main.ts
|
|
183
|
-
import { fileURLToPath } from "url";
|
|
184
|
-
import { argv } from "process";
|
|
185
|
-
import { realpathSync } from "fs";
|
|
186
|
-
|
|
187
|
-
// src/cli/defaults.ts
|
|
188
|
-
var defaultOptions = {
|
|
189
|
-
version: "1.0",
|
|
190
|
-
token: void 0,
|
|
191
|
-
// No default token
|
|
192
|
-
serverAddress: "a.pinggy.io",
|
|
193
|
-
forwarding: "localhost:8000",
|
|
194
|
-
webDebugger: "",
|
|
195
|
-
ipWhitelist: [],
|
|
196
|
-
basicAuth: [],
|
|
197
|
-
bearerTokenAuth: [],
|
|
198
|
-
headerModification: [],
|
|
199
|
-
force: false,
|
|
200
|
-
xForwardedFor: false,
|
|
201
|
-
httpsOnly: false,
|
|
202
|
-
originalRequestUrl: false,
|
|
203
|
-
allowPreflight: false,
|
|
204
|
-
reverseProxy: false,
|
|
205
|
-
autoReconnect: true
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
// src/cli/extendedOptions.ts
|
|
209
|
-
import { isIP } from "net";
|
|
210
|
-
function parseExtendedOptions(options, config, localServerTls) {
|
|
211
|
-
if (!options) return localServerTls;
|
|
212
|
-
for (const opt of options) {
|
|
213
|
-
const [key, value] = opt.replace(/^"|"$/g, "").split(/:(.+)/).filter(Boolean);
|
|
214
|
-
switch (key) {
|
|
215
|
-
case "x":
|
|
216
|
-
switch (value) {
|
|
217
|
-
case "https":
|
|
218
|
-
case "httpsonly":
|
|
219
|
-
config.httpsOnly = true;
|
|
220
|
-
break;
|
|
221
|
-
case "passpreflight":
|
|
222
|
-
case "allowpreflight":
|
|
223
|
-
config.allowPreflight = true;
|
|
224
|
-
break;
|
|
225
|
-
case "reverseproxy":
|
|
226
|
-
config.reverseProxy = false;
|
|
227
|
-
break;
|
|
228
|
-
case "xff":
|
|
229
|
-
config.xForwardedFor = true;
|
|
230
|
-
break;
|
|
231
|
-
case "fullurl":
|
|
232
|
-
case "fullrequesturl":
|
|
233
|
-
config.originalRequestUrl = true;
|
|
234
|
-
break;
|
|
235
|
-
default: {
|
|
236
|
-
if (value && (value.startsWith("localServerTls") || value.startsWith("localservertls"))) {
|
|
237
|
-
const parts = value.split(/:(.+)/);
|
|
238
|
-
localServerTls = parts[1] ? parts[1] : "";
|
|
239
|
-
} else {
|
|
240
|
-
printer_default.warn(`Unknown extended option "${value}"`);
|
|
241
|
-
logger.warn(`Warning: Unknown extended option "${value}"`);
|
|
242
|
-
}
|
|
243
|
-
break;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
break;
|
|
247
|
-
case "w":
|
|
248
|
-
if (value) {
|
|
249
|
-
const ips = value.split(",").map((ip) => ip.trim()).filter(Boolean);
|
|
250
|
-
const invalidIps = ips.filter((ip) => !(isValidIpV4Cidr(ip) || isValidIpV6Cidr(ip)));
|
|
251
|
-
if (invalidIps.length > 0) {
|
|
252
|
-
printer_default.warn(`Invalid IP/CIDR(s) in whitelist: ${invalidIps.join(", ")}`);
|
|
253
|
-
logger.warn(`Warning: Invalid IP/CIDR(s) in whitelist: ${invalidIps.join(", ")}`);
|
|
254
|
-
}
|
|
255
|
-
if (!(invalidIps.length > 0)) {
|
|
256
|
-
config.ipWhitelist = ips;
|
|
257
|
-
}
|
|
258
|
-
} else {
|
|
259
|
-
printer_default.warn(`Extended option "${opt}" for 'w' requires IP(s)`);
|
|
260
|
-
logger.warn(`Warning: Extended option "${opt}" for 'w' requires IP(s)`);
|
|
261
|
-
}
|
|
262
|
-
break;
|
|
263
|
-
case "k":
|
|
264
|
-
if (!config.bearerTokenAuth) config.bearerTokenAuth = [];
|
|
265
|
-
if (value) {
|
|
266
|
-
config.bearerTokenAuth.push(value);
|
|
267
|
-
} else {
|
|
268
|
-
printer_default.warn(`Extended option "${opt}" for 'k' requires a value`);
|
|
269
|
-
logger.warn(`Warning: Extended option "${opt}" for 'k' requires a value`);
|
|
270
|
-
}
|
|
271
|
-
break;
|
|
272
|
-
case "b":
|
|
273
|
-
if (value && value.includes(":")) {
|
|
274
|
-
const [username, password] = value.split(/:(.+)/);
|
|
275
|
-
if (!config.basicAuth) config.basicAuth = [];
|
|
276
|
-
config.basicAuth.push({ username, password });
|
|
277
|
-
} else {
|
|
278
|
-
printer_default.warn(`Extended option "${opt}" for 'b' requires value in format username:password`);
|
|
279
|
-
logger.warn(`Warning: Extended option "${opt}" for 'b' requires value in format username:password`);
|
|
280
|
-
}
|
|
281
|
-
break;
|
|
282
|
-
case "a":
|
|
283
|
-
if (value && value.includes(":")) {
|
|
284
|
-
const [key2, val] = value.split(/:(.+)/);
|
|
285
|
-
if (!config.headerModification) config.headerModification = [];
|
|
286
|
-
config.headerModification.push({ type: "add", key: key2, value: [val] });
|
|
287
|
-
} else {
|
|
288
|
-
printer_default.warn(`Extended option "${opt}" for 'a' requires key:value`);
|
|
289
|
-
logger.warn(`Warning: Extended option "${opt}" for 'a' requires key:value`);
|
|
290
|
-
}
|
|
291
|
-
break;
|
|
292
|
-
case "u":
|
|
293
|
-
if (value && value.includes(":")) {
|
|
294
|
-
const [key2, val] = value.split(/:(.+)/);
|
|
295
|
-
if (!config.headerModification) config.headerModification = [];
|
|
296
|
-
config.headerModification.push({ type: "update", key: key2, value: [val] });
|
|
297
|
-
} else {
|
|
298
|
-
printer_default.warn(`Extended option "${opt}" for 'u' requires key:value`);
|
|
299
|
-
logger.warn(`Warning: Extended option "${opt}" for 'u' requires key:value`);
|
|
300
|
-
}
|
|
301
|
-
break;
|
|
302
|
-
case "r":
|
|
303
|
-
if (value) {
|
|
304
|
-
if (!config.headerModification) config.headerModification = [];
|
|
305
|
-
config.headerModification.push({ type: "remove", key: value, value: [] });
|
|
306
|
-
} else {
|
|
307
|
-
printer_default.warn(`Extended option "${opt}" for 'r' requires a key`);
|
|
308
|
-
}
|
|
309
|
-
break;
|
|
310
|
-
default:
|
|
311
|
-
printer_default.warn(`Unknown extended option "${key}"`);
|
|
312
|
-
break;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
return localServerTls;
|
|
316
|
-
}
|
|
317
|
-
function isValidIpV4Cidr(input) {
|
|
318
|
-
if (input.includes("/")) {
|
|
319
|
-
const [ip, mask] = input.split("/");
|
|
320
|
-
if (!ip || !mask) return false;
|
|
321
|
-
const isIp4 = isIP(ip) === 4;
|
|
322
|
-
const maskNum = parseInt(mask, 10);
|
|
323
|
-
const isMaskValid = !isNaN(maskNum) && maskNum >= 0 && maskNum <= 32;
|
|
324
|
-
return isIp4 && isMaskValid;
|
|
325
|
-
}
|
|
326
|
-
return false;
|
|
327
|
-
}
|
|
328
|
-
function isValidIpV6Cidr(input) {
|
|
329
|
-
if (input.includes("/")) {
|
|
330
|
-
const [rawIp, mask] = input.split("/");
|
|
331
|
-
if (!rawIp || !mask) return false;
|
|
332
|
-
const ip = rawIp.split("%")[0].replace(/^\[|\]$/g, "");
|
|
333
|
-
const isIp6 = isIP(ip) === 6;
|
|
334
|
-
const maskNum = parseInt(mask, 10);
|
|
335
|
-
const isMaskValid = !isNaN(maskNum) && maskNum >= 0 && maskNum <= 128;
|
|
336
|
-
return isIp6 && isMaskValid;
|
|
337
|
-
}
|
|
338
|
-
return false;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// src/cli/buildConfig.ts
|
|
342
|
-
import { TunnelType } from "@pinggy/pinggy";
|
|
343
|
-
import fs from "fs";
|
|
344
|
-
import path from "path";
|
|
345
|
-
import { isIP as isIP2 } from "net";
|
|
346
|
-
var domainRegex = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
|
|
347
|
-
function removeIPv6Brackets(ip) {
|
|
348
|
-
if (ip.startsWith("[") && ip.endsWith("]")) {
|
|
349
|
-
return ip.slice(1, -1);
|
|
350
|
-
}
|
|
351
|
-
return ip;
|
|
352
|
-
}
|
|
353
|
-
function isValidServerAddress(host) {
|
|
354
|
-
const normalized = removeIPv6Brackets(host.trim());
|
|
355
|
-
if (!normalized) {
|
|
356
|
-
return false;
|
|
357
|
-
}
|
|
358
|
-
return domainRegex.test(normalized) || isIP2(normalized) !== 0;
|
|
359
|
-
}
|
|
360
|
-
var KEYWORDS = /* @__PURE__ */ new Set([
|
|
361
|
-
TunnelType.Http,
|
|
362
|
-
TunnelType.Tcp,
|
|
363
|
-
TunnelType.Tls,
|
|
364
|
-
TunnelType.Udp,
|
|
365
|
-
TunnelType.TlsTcp,
|
|
366
|
-
"force",
|
|
367
|
-
"qr"
|
|
368
|
-
]);
|
|
369
|
-
function isKeyword(str) {
|
|
370
|
-
return KEYWORDS.has(str.toLowerCase());
|
|
371
|
-
}
|
|
372
|
-
function parseUserAndDomain(str) {
|
|
373
|
-
let token;
|
|
374
|
-
let type;
|
|
375
|
-
let server;
|
|
376
|
-
let qrCode;
|
|
377
|
-
let forceFlag;
|
|
378
|
-
if (!str) {
|
|
379
|
-
return { token, type, server, qrCode, forceFlag };
|
|
380
|
-
}
|
|
381
|
-
if (str.includes("@")) {
|
|
382
|
-
const [user, domain] = str.split("@", 2);
|
|
383
|
-
if (isValidServerAddress(domain)) {
|
|
384
|
-
let processKeyword2 = function(keyword) {
|
|
385
|
-
if ([TunnelType.Http, TunnelType.Tcp, TunnelType.Tls, TunnelType.Udp, TunnelType.TlsTcp].includes(keyword)) {
|
|
386
|
-
type = keyword;
|
|
387
|
-
} else if (keyword === "force") {
|
|
388
|
-
forceFlag = true;
|
|
389
|
-
} else if (keyword === "qr") {
|
|
390
|
-
qrCode = true;
|
|
391
|
-
}
|
|
392
|
-
};
|
|
393
|
-
var processKeyword = processKeyword2;
|
|
394
|
-
server = domain;
|
|
395
|
-
const parts = user.split("+");
|
|
396
|
-
if (parts.length === 0) {
|
|
397
|
-
return { token, type, server, qrCode, forceFlag };
|
|
398
|
-
}
|
|
399
|
-
const firstPart = parts[0];
|
|
400
|
-
if (!isKeyword(firstPart)) {
|
|
401
|
-
token = firstPart;
|
|
402
|
-
for (let i = 1; i < parts.length; i++) {
|
|
403
|
-
const part = parts[i].toLowerCase();
|
|
404
|
-
if (!isKeyword(part)) {
|
|
405
|
-
throw new Error(`Invalid user format: unexpected token '${part}' when keywords are expected.`);
|
|
406
|
-
}
|
|
407
|
-
processKeyword2(part);
|
|
408
|
-
}
|
|
409
|
-
} else {
|
|
410
|
-
for (const part of parts) {
|
|
411
|
-
const lowerPart = part.toLowerCase();
|
|
412
|
-
if (!isKeyword(lowerPart)) {
|
|
413
|
-
throw new Error(`Invalid user format: unexpected token '${lowerPart}' when keywords are expected.`);
|
|
414
|
-
}
|
|
415
|
-
processKeyword2(lowerPart);
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
} else if (isValidServerAddress(str)) {
|
|
420
|
-
server = str;
|
|
421
|
-
}
|
|
422
|
-
return { token, type, server, qrCode, forceFlag };
|
|
423
|
-
}
|
|
424
|
-
function parseUsers(positionalArgs, explicitToken) {
|
|
425
|
-
let token;
|
|
426
|
-
let server;
|
|
427
|
-
let type;
|
|
428
|
-
let forceFlag = false;
|
|
429
|
-
let qrCode = false;
|
|
430
|
-
let remaining = [...positionalArgs];
|
|
431
|
-
if (typeof explicitToken === "string") {
|
|
432
|
-
const parsed = parseUserAndDomain(explicitToken);
|
|
433
|
-
if (parsed.server) {
|
|
434
|
-
server = parsed.server;
|
|
435
|
-
}
|
|
436
|
-
if (parsed.type) {
|
|
437
|
-
type = parsed.type;
|
|
438
|
-
}
|
|
439
|
-
if (parsed.token) {
|
|
440
|
-
token = parsed.token;
|
|
441
|
-
}
|
|
442
|
-
if (parsed.forceFlag) {
|
|
443
|
-
forceFlag = true;
|
|
444
|
-
}
|
|
445
|
-
if (parsed.qrCode) {
|
|
446
|
-
qrCode = true;
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
if (remaining.length > 0) {
|
|
450
|
-
const first = remaining[0];
|
|
451
|
-
const parsed = parseUserAndDomain(first);
|
|
452
|
-
if (parsed.server) {
|
|
453
|
-
server = parsed.server;
|
|
454
|
-
if (parsed.type) {
|
|
455
|
-
type = parsed.type;
|
|
456
|
-
}
|
|
457
|
-
if (parsed.token) {
|
|
458
|
-
token = parsed.token;
|
|
459
|
-
}
|
|
460
|
-
if (parsed.forceFlag) {
|
|
461
|
-
forceFlag = true;
|
|
462
|
-
}
|
|
463
|
-
if (parsed.qrCode) {
|
|
464
|
-
qrCode = true;
|
|
465
|
-
}
|
|
466
|
-
remaining = remaining.slice(1);
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
return { token, server, type, forceFlag, qrCode, remaining };
|
|
470
|
-
}
|
|
471
|
-
function parseType(finalConfig, values, inferredType) {
|
|
472
|
-
const t = inferredType || values.type;
|
|
473
|
-
if (t === TunnelType.Http || t === TunnelType.Tcp || t === TunnelType.Tls || t === TunnelType.Udp || t === TunnelType.TlsTcp) {
|
|
474
|
-
return t;
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
function parseLocalPort(finalConfig, values) {
|
|
478
|
-
if (typeof values.localport !== "string") {
|
|
479
|
-
return null;
|
|
480
|
-
}
|
|
481
|
-
let lp = values.localport.trim();
|
|
482
|
-
let isHttps = false;
|
|
483
|
-
if (lp.startsWith("https://")) {
|
|
484
|
-
isHttps = true;
|
|
485
|
-
lp = lp.replace(/^https:\/\//, "");
|
|
486
|
-
} else if (lp.startsWith("http://")) {
|
|
487
|
-
lp = lp.replace(/^http:\/\//, "");
|
|
488
|
-
}
|
|
489
|
-
const parts = lp.split(":");
|
|
490
|
-
if (parts.length === 1) {
|
|
491
|
-
const port = parseInt(parts[0], 10);
|
|
492
|
-
if (!Number.isNaN(port) && isValidPort(port)) {
|
|
493
|
-
finalConfig.forwarding = `localhost:${port}`;
|
|
494
|
-
} else {
|
|
495
|
-
return new Error("Invalid local port");
|
|
496
|
-
}
|
|
497
|
-
} else if (parts.length === 2) {
|
|
498
|
-
const host = parts[0] || "localhost";
|
|
499
|
-
const port = parseInt(parts[1], 10);
|
|
500
|
-
if (!Number.isNaN(port) && isValidPort(port)) {
|
|
501
|
-
finalConfig.forwarding = `${host}:${port}`;
|
|
502
|
-
} else {
|
|
503
|
-
return new Error("Invalid local port. Please use -h option for help.");
|
|
504
|
-
}
|
|
505
|
-
} else {
|
|
506
|
-
return new Error("Invalid --localport format. Please use -h option for help.");
|
|
507
|
-
}
|
|
508
|
-
return null;
|
|
509
|
-
}
|
|
510
|
-
function isValidHostAddress(host) {
|
|
511
|
-
const normalized = removeIPv6Brackets(host.trim());
|
|
512
|
-
if (normalized.length === 0) {
|
|
513
|
-
return false;
|
|
514
|
-
}
|
|
515
|
-
return normalized === "localhost" || isIP2(normalized) !== 0;
|
|
516
|
-
}
|
|
517
|
-
function ipv6SafeSplitColon(s) {
|
|
518
|
-
const result = [];
|
|
519
|
-
let buf = "";
|
|
520
|
-
const stack = [];
|
|
521
|
-
for (let i = 0; i < s.length; i++) {
|
|
522
|
-
const c = s[i];
|
|
523
|
-
if (c === "[") {
|
|
524
|
-
stack.push(c);
|
|
525
|
-
} else if (c === "]" && stack.length > 0) {
|
|
526
|
-
stack.pop();
|
|
527
|
-
}
|
|
528
|
-
if (c === ":" && stack.length === 0) {
|
|
529
|
-
result.push(buf);
|
|
530
|
-
buf = "";
|
|
531
|
-
} else {
|
|
532
|
-
buf += c;
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
result.push(buf);
|
|
536
|
-
return result;
|
|
537
|
-
}
|
|
538
|
-
var VALID_PROTOCOLS = [TunnelType.Http, TunnelType.Tcp, TunnelType.Udp, TunnelType.Tls, TunnelType.TlsTcp];
|
|
539
|
-
function parseDefaultForwarding(forwarding) {
|
|
540
|
-
const parts = ipv6SafeSplitColon(forwarding);
|
|
541
|
-
if (parts.length === 3) {
|
|
542
|
-
const remotePort = parseInt(parts[0], 10);
|
|
543
|
-
const localDomain = removeIPv6Brackets(parts[1] || "localhost");
|
|
544
|
-
const localPort = parseInt(parts[2], 10);
|
|
545
|
-
return { remotePort, localDomain, localPort };
|
|
546
|
-
}
|
|
547
|
-
if (parts.length === 4) {
|
|
548
|
-
const remoteDomain = removeIPv6Brackets(parts[0]);
|
|
549
|
-
const remotePort = parseInt(parts[1], 10);
|
|
550
|
-
const localDomain = removeIPv6Brackets(parts[2] || "localhost");
|
|
551
|
-
const localPort = parseInt(parts[3], 10);
|
|
552
|
-
return { remoteDomain, remotePort, localDomain, localPort };
|
|
553
|
-
}
|
|
554
|
-
return new Error("forwarding address incorrect");
|
|
555
|
-
}
|
|
556
|
-
function parseAdditionalForwarding(forwarding) {
|
|
557
|
-
const toPort = (v) => {
|
|
558
|
-
if (!v) {
|
|
559
|
-
return null;
|
|
560
|
-
}
|
|
561
|
-
const n = parseInt(v, 10);
|
|
562
|
-
return Number.isNaN(n) ? null : n;
|
|
563
|
-
};
|
|
564
|
-
const parsed = ipv6SafeSplitColon(forwarding);
|
|
565
|
-
if (parsed.length !== 4) {
|
|
566
|
-
return new Error(
|
|
567
|
-
"forwarding must be in format: [schema//]hostname[/port][@forwardingId]:<placeholder>:<forwardingAddress>:<forwardingPort>"
|
|
568
|
-
);
|
|
569
|
-
}
|
|
570
|
-
const firstPart = parsed[0];
|
|
571
|
-
const [hostPart] = firstPart.split("@");
|
|
572
|
-
let protocol = TunnelType.Http;
|
|
573
|
-
let remoteDomainRaw;
|
|
574
|
-
let remotePort = 0;
|
|
575
|
-
if (hostPart.includes("//")) {
|
|
576
|
-
const [schema, rest] = hostPart.split("//");
|
|
577
|
-
if (!schema || !VALID_PROTOCOLS.includes(schema)) {
|
|
578
|
-
return new Error(`invalid protocol: ${schema}`);
|
|
579
|
-
}
|
|
580
|
-
protocol = schema;
|
|
581
|
-
const domainAndPort = rest.split("/");
|
|
582
|
-
if (domainAndPort.length > 2) {
|
|
583
|
-
return new Error("invalid forwarding address format");
|
|
584
|
-
}
|
|
585
|
-
remoteDomainRaw = domainAndPort[0];
|
|
586
|
-
if (!remoteDomainRaw || !isValidServerAddress(remoteDomainRaw)) {
|
|
587
|
-
return new Error("invalid remote domain");
|
|
588
|
-
}
|
|
589
|
-
const parsedRemotePort = toPort(domainAndPort[1]);
|
|
590
|
-
if (protocol === "http") {
|
|
591
|
-
remotePort = 0;
|
|
592
|
-
} else {
|
|
593
|
-
if (parsedRemotePort === null || !isValidPort(parsedRemotePort)) {
|
|
594
|
-
return new Error(
|
|
595
|
-
`${protocol} forwarding requires port in format ${protocol}//domain/remotePort`
|
|
596
|
-
);
|
|
597
|
-
}
|
|
598
|
-
remotePort = parsedRemotePort;
|
|
599
|
-
}
|
|
600
|
-
} else {
|
|
601
|
-
remoteDomainRaw = hostPart;
|
|
602
|
-
if (!isValidServerAddress(remoteDomainRaw)) {
|
|
603
|
-
return new Error("invalid remote domain");
|
|
604
|
-
}
|
|
605
|
-
protocol = TunnelType.Http;
|
|
606
|
-
remotePort = 0;
|
|
607
|
-
}
|
|
608
|
-
const localDomain = removeIPv6Brackets(parsed[2] || "localhost");
|
|
609
|
-
const localPort = toPort(parsed[3]);
|
|
610
|
-
if (localPort === null || !isValidPort(localPort)) {
|
|
611
|
-
return new Error("forwarding address incorrect: invalid local port");
|
|
612
|
-
}
|
|
613
|
-
return {
|
|
614
|
-
type: protocol,
|
|
615
|
-
listenAddress: `${remoteDomainRaw}:${remotePort}`,
|
|
616
|
-
address: `${localDomain}:${localPort}`
|
|
617
|
-
};
|
|
618
|
-
}
|
|
619
|
-
function parseReverseTunnelAddr(finalConfig, values, primaryType) {
|
|
620
|
-
const reverseTunnel = values.R;
|
|
621
|
-
let forwardingData = [];
|
|
622
|
-
if ((!Array.isArray(reverseTunnel) || reverseTunnel.length === 0) && !values.localport && !finalConfig.forwarding) {
|
|
623
|
-
return new Error("local port not specified. Please use '-h' option for help.");
|
|
624
|
-
}
|
|
625
|
-
if (!Array.isArray(reverseTunnel) || reverseTunnel.length === 0) {
|
|
626
|
-
return null;
|
|
627
|
-
}
|
|
628
|
-
for (const forwarding of reverseTunnel) {
|
|
629
|
-
const slicedForwarding = ipv6SafeSplitColon(forwarding);
|
|
630
|
-
if (slicedForwarding.length === 3) {
|
|
631
|
-
const parsed = parseDefaultForwarding(forwarding);
|
|
632
|
-
if (parsed instanceof Error) return parsed;
|
|
633
|
-
forwardingData.push({
|
|
634
|
-
address: `${parsed.localDomain}:${parsed.localPort}`,
|
|
635
|
-
type: primaryType || TunnelType.Http
|
|
636
|
-
});
|
|
637
|
-
} else if (slicedForwarding.length === 4) {
|
|
638
|
-
const parsed = parseAdditionalForwarding(forwarding);
|
|
639
|
-
if (parsed instanceof Error) {
|
|
640
|
-
return parsed;
|
|
641
|
-
}
|
|
642
|
-
forwardingData.push(parsed);
|
|
643
|
-
} else {
|
|
644
|
-
return new Error(
|
|
645
|
-
"Incorrect command line arguments: reverse tunnel address incorrect. Please use '-h' option for help."
|
|
646
|
-
);
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
finalConfig.forwarding = forwardingData;
|
|
650
|
-
return null;
|
|
651
|
-
}
|
|
652
|
-
function parseLocalTunnelAddr(finalConfig, values) {
|
|
653
|
-
if (!Array.isArray(values.L) || values.L.length === 0) {
|
|
654
|
-
return null;
|
|
655
|
-
}
|
|
656
|
-
const firstL = values.L[0];
|
|
657
|
-
const parts = ipv6SafeSplitColon(firstL);
|
|
658
|
-
let debuggerHost = "localhost";
|
|
659
|
-
let lp;
|
|
660
|
-
if (parts.length === 3) {
|
|
661
|
-
lp = parseInt(parts[0], 10);
|
|
662
|
-
} else if (parts.length === 4) {
|
|
663
|
-
debuggerHost = removeIPv6Brackets(parts[0]);
|
|
664
|
-
lp = parseInt(parts[1], 10);
|
|
665
|
-
} else {
|
|
666
|
-
return new Error("Incorrect command line arguments: web debugger address incorrect. Please use '-h' option for help.");
|
|
667
|
-
}
|
|
668
|
-
if (!isValidHostAddress(debuggerHost)) {
|
|
669
|
-
return new Error(`Invalid debugger host ${debuggerHost}. Please use localhost, IPv4, or IPv6 address.`);
|
|
670
|
-
}
|
|
671
|
-
if (!Number.isNaN(lp) && isValidPort(lp)) {
|
|
672
|
-
finalConfig.webDebugger = `${debuggerHost}:${lp}`;
|
|
673
|
-
} else {
|
|
674
|
-
return new Error(`Invalid debugger port ${lp}`);
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
function parseDebugger(finalConfig, values) {
|
|
678
|
-
let dbg = values.debugger;
|
|
679
|
-
if (typeof dbg !== "string") {
|
|
680
|
-
return;
|
|
681
|
-
}
|
|
682
|
-
dbg = dbg.startsWith(":") ? dbg.slice(1) : dbg;
|
|
683
|
-
const d = parseInt(dbg, 10);
|
|
684
|
-
if (!Number.isNaN(d) && isValidPort(d)) {
|
|
685
|
-
finalConfig.webDebugger = `localhost:${d}`;
|
|
686
|
-
} else {
|
|
687
|
-
logger.error("Invalid debugger port:", dbg);
|
|
688
|
-
return new Error(`Invalid debugger port ${dbg}. Please use '-h' option for help.`);
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
function parseToken(finalConfig, explicitToken) {
|
|
692
|
-
if (typeof explicitToken === "string" && explicitToken) {
|
|
693
|
-
finalConfig.token = explicitToken;
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
function parseArgs2(finalConfig, remainingPositionals) {
|
|
697
|
-
let localserverTls = "";
|
|
698
|
-
localserverTls = parseExtendedOptions(remainingPositionals, finalConfig, localserverTls);
|
|
699
|
-
if (localserverTls.length > 0 && finalConfig.forwarding) {
|
|
700
|
-
if (typeof finalConfig.forwarding[0] === "object" && "address" in finalConfig.forwarding[0]) {
|
|
701
|
-
finalConfig.forwarding[0].address = `https://${finalConfig.forwarding[0].address}`;
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
function storeJson(config, saveconf) {
|
|
706
|
-
if (saveconf) {
|
|
707
|
-
const path4 = saveconf;
|
|
708
|
-
try {
|
|
709
|
-
fs.writeFileSync(path4, JSON.stringify(config, null, 2), { encoding: "utf-8", flag: "w" });
|
|
710
|
-
logger.info(`Configuration saved to ${path4}`);
|
|
711
|
-
} catch (err) {
|
|
712
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
713
|
-
logger.error("Error loading configuration:", msg);
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
function loadJsonConfig(config) {
|
|
718
|
-
const configpath = config["conf"];
|
|
719
|
-
if (typeof configpath === "string" && configpath.trim().length > 0) {
|
|
720
|
-
const filepath = path.resolve(configpath);
|
|
721
|
-
try {
|
|
722
|
-
const data = fs.readFileSync(filepath, { encoding: "utf-8" });
|
|
723
|
-
const json = JSON.parse(data);
|
|
724
|
-
return json;
|
|
725
|
-
} catch (err) {
|
|
726
|
-
logger.error("Error loading configuration:", err);
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
return null;
|
|
730
|
-
}
|
|
731
|
-
function isSaveConfOption(values) {
|
|
732
|
-
const saveconf = values["saveconf"];
|
|
733
|
-
if (typeof saveconf === "string" && saveconf.trim().length > 0) {
|
|
734
|
-
return saveconf;
|
|
735
|
-
}
|
|
736
|
-
return null;
|
|
737
|
-
}
|
|
738
|
-
function parseServe(finalConfig, values) {
|
|
739
|
-
const sv = values.serve;
|
|
740
|
-
if (typeof sv !== "string" || sv.trim().length === 0) {
|
|
741
|
-
return null;
|
|
742
|
-
}
|
|
743
|
-
finalConfig.optional.serve = sv;
|
|
744
|
-
return null;
|
|
745
|
-
}
|
|
746
|
-
function parseAutoReconnect(finalConfig, values) {
|
|
747
|
-
if (values["no-autoreconnect"]) {
|
|
748
|
-
finalConfig.autoReconnect = false;
|
|
749
|
-
}
|
|
750
|
-
return null;
|
|
751
|
-
}
|
|
752
|
-
async function buildFinalConfig(values, positionals, baseConfig) {
|
|
753
|
-
let token;
|
|
754
|
-
let server;
|
|
755
|
-
let type;
|
|
756
|
-
let forceFlag = false;
|
|
757
|
-
let qrCode = false;
|
|
758
|
-
let finalConfig = new Object();
|
|
759
|
-
let saveconf = isSaveConfOption(values);
|
|
760
|
-
const configFromFile = baseConfig || loadJsonConfig(values);
|
|
761
|
-
const userParse = parseUsers(positionals, values.token);
|
|
762
|
-
token = userParse.token;
|
|
763
|
-
server = userParse.server;
|
|
764
|
-
type = userParse.type;
|
|
765
|
-
forceFlag = userParse.forceFlag;
|
|
766
|
-
qrCode = userParse.qrCode;
|
|
767
|
-
const remainingPositionals = userParse.remaining;
|
|
768
|
-
const initialTunnel = type || values.type;
|
|
769
|
-
finalConfig = {
|
|
770
|
-
...defaultOptions,
|
|
771
|
-
...configFromFile || {},
|
|
772
|
-
// Apply loaded config on top of defaults
|
|
773
|
-
configId: getRandomId(),
|
|
774
|
-
token: token || (configFromFile?.token || (typeof values.token === "string" ? values.token : "")),
|
|
775
|
-
serverAddress: server ? removeIPv6Brackets(server) : configFromFile?.serverAddress || defaultOptions.serverAddress,
|
|
776
|
-
isQRCode: qrCode || (configFromFile?.isQRCode || false),
|
|
777
|
-
autoReconnect: configFromFile?.autoReconnect ? configFromFile.autoReconnect : defaultOptions.autoReconnect,
|
|
778
|
-
optional: {
|
|
779
|
-
serve: configFromFile?.optional?.serve || void 0,
|
|
780
|
-
noTui: values.noTui || values.notui || (configFromFile?.optional?.noTui || false)
|
|
781
|
-
}
|
|
782
|
-
};
|
|
783
|
-
type = parseType(finalConfig, values, type);
|
|
784
|
-
parseToken(finalConfig, token || values.token);
|
|
785
|
-
const dbgErr = parseDebugger(finalConfig, values);
|
|
786
|
-
if (dbgErr instanceof Error) {
|
|
787
|
-
throw dbgErr;
|
|
788
|
-
}
|
|
789
|
-
const lpErr = parseLocalPort(finalConfig, values);
|
|
790
|
-
if (lpErr instanceof Error) {
|
|
791
|
-
throw lpErr;
|
|
792
|
-
}
|
|
793
|
-
const rErr = parseReverseTunnelAddr(finalConfig, values, type);
|
|
794
|
-
if (rErr instanceof Error) {
|
|
795
|
-
throw rErr;
|
|
796
|
-
}
|
|
797
|
-
const lErr = parseLocalTunnelAddr(finalConfig, values);
|
|
798
|
-
if (lErr instanceof Error) {
|
|
799
|
-
throw lErr;
|
|
800
|
-
}
|
|
801
|
-
const serveErr = parseServe(finalConfig, values);
|
|
802
|
-
if (serveErr instanceof Error) {
|
|
803
|
-
throw serveErr;
|
|
804
|
-
}
|
|
805
|
-
const autoReconnectErr = parseAutoReconnect(finalConfig, values);
|
|
806
|
-
if (autoReconnectErr instanceof Error) {
|
|
807
|
-
throw autoReconnectErr;
|
|
808
|
-
}
|
|
809
|
-
if (forceFlag || values.force) {
|
|
810
|
-
finalConfig.force = true;
|
|
811
|
-
}
|
|
812
|
-
parseArgs2(finalConfig, remainingPositionals);
|
|
813
|
-
storeJson(finalConfig, saveconf);
|
|
814
|
-
return finalConfig;
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
// src/utils/getFreePort.ts
|
|
818
|
-
import net from "net";
|
|
819
|
-
function getFreePort(webDebugger) {
|
|
820
|
-
return new Promise((resolve, reject) => {
|
|
821
|
-
const tryPort = (portToTry) => {
|
|
822
|
-
const server = net.createServer();
|
|
823
|
-
server.unref();
|
|
824
|
-
server.on("error", (err) => {
|
|
825
|
-
if (portToTry !== 0) {
|
|
826
|
-
tryPort(0);
|
|
827
|
-
} else {
|
|
828
|
-
reject(err);
|
|
829
|
-
}
|
|
830
|
-
});
|
|
831
|
-
server.listen(portToTry, () => {
|
|
832
|
-
const address = server.address();
|
|
833
|
-
const port = address ? address.port : 0;
|
|
834
|
-
server.close(() => resolve(port));
|
|
835
|
-
});
|
|
836
|
-
};
|
|
837
|
-
let providedPort = 0;
|
|
838
|
-
if (webDebugger && webDebugger.includes(":")) {
|
|
839
|
-
const portPart = webDebugger.split(":")[1];
|
|
840
|
-
const parsed = parseInt(portPart, 10);
|
|
841
|
-
if (!isNaN(parsed) && parsed > 0 && parsed < 65536) {
|
|
842
|
-
providedPort = parsed;
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
tryPort(providedPort);
|
|
846
|
-
});
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
// src/cli/starCli.ts
|
|
850
|
-
import pico from "picocolors";
|
|
851
|
-
|
|
852
|
-
// src/tui/blessed/TunnelTui.ts
|
|
853
|
-
import blessed3 from "blessed";
|
|
854
|
-
|
|
855
|
-
// src/tui/blessed/qrCodeGenerator.ts
|
|
856
|
-
import QRCode from "qrcode";
|
|
857
|
-
async function createQrCodes(urls) {
|
|
858
|
-
const codes = [];
|
|
859
|
-
for (const url of urls) {
|
|
860
|
-
const raw = await QRCode.toString(url, {
|
|
861
|
-
type: "utf8",
|
|
862
|
-
margin: 2,
|
|
863
|
-
errorCorrectionLevel: "L"
|
|
864
|
-
});
|
|
865
|
-
codes.push(raw);
|
|
866
|
-
}
|
|
867
|
-
return codes;
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
// src/tui/blessed/webDebuggerConnection.ts
|
|
871
|
-
import WebSocket from "ws";
|
|
872
|
-
|
|
873
|
-
// src/tui/blessed/config.ts
|
|
874
|
-
var defaultTuiConfig = {
|
|
875
|
-
maxRequestPairs: 100,
|
|
876
|
-
visibleRequestCount: 10,
|
|
877
|
-
visibleUrlCount: 7,
|
|
878
|
-
viewportScrollMargin: 2,
|
|
879
|
-
inactivityHttpSelectorTimeoutMs: 1e4
|
|
880
|
-
};
|
|
881
|
-
function getTuiConfig() {
|
|
882
|
-
return {
|
|
883
|
-
maxRequestPairs: defaultTuiConfig.maxRequestPairs,
|
|
884
|
-
visibleRequestCount: defaultTuiConfig.visibleRequestCount,
|
|
885
|
-
visibleUrlCount: defaultTuiConfig.visibleUrlCount,
|
|
886
|
-
viewportScrollMargin: defaultTuiConfig.viewportScrollMargin,
|
|
887
|
-
inactivityHttpSelectorTimeoutMs: defaultTuiConfig.inactivityHttpSelectorTimeoutMs
|
|
888
|
-
};
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
// src/tui/blessed/webDebuggerConnection.ts
|
|
892
|
-
function createWebDebuggerConnection(webDebuggerUrl, onUpdate) {
|
|
893
|
-
const pairs = /* @__PURE__ */ new Map();
|
|
894
|
-
const pairKeys = [];
|
|
895
|
-
let socket = null;
|
|
896
|
-
let reconnectTimeout = null;
|
|
897
|
-
let isStopped = false;
|
|
898
|
-
const config = getTuiConfig();
|
|
899
|
-
const maxPairs = config.maxRequestPairs;
|
|
900
|
-
const trimPairs = () => {
|
|
901
|
-
while (pairKeys.length > maxPairs) {
|
|
902
|
-
const oldestKey = pairKeys.shift();
|
|
903
|
-
if (oldestKey !== void 0) {
|
|
904
|
-
pairs.delete(oldestKey);
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
};
|
|
908
|
-
const upsertPair = (key, pair) => {
|
|
909
|
-
if (!pairs.has(key)) {
|
|
910
|
-
pairKeys.push(key);
|
|
911
|
-
}
|
|
912
|
-
pairs.set(key, pair);
|
|
913
|
-
trimPairs();
|
|
914
|
-
};
|
|
915
|
-
const connect = () => {
|
|
916
|
-
const ws = new WebSocket(`ws://${webDebuggerUrl}/introspec/websocket`);
|
|
917
|
-
socket = ws;
|
|
918
|
-
ws.on("open", () => {
|
|
919
|
-
logger.info("Web debugger connected.");
|
|
920
|
-
});
|
|
921
|
-
ws.on("message", (data) => {
|
|
922
|
-
try {
|
|
923
|
-
const raw = data.toString();
|
|
924
|
-
const parsed = JSON.parse(raw);
|
|
925
|
-
const msg = {
|
|
926
|
-
Req: parsed.req,
|
|
927
|
-
Res: parsed.res
|
|
928
|
-
};
|
|
929
|
-
if (msg.Req) {
|
|
930
|
-
const { key } = msg.Req;
|
|
931
|
-
const existing = pairs.get(key);
|
|
932
|
-
const merged = {
|
|
933
|
-
request: msg.Req,
|
|
934
|
-
response: existing?.response
|
|
935
|
-
};
|
|
936
|
-
upsertPair(key, merged);
|
|
937
|
-
}
|
|
938
|
-
if (msg.Res) {
|
|
939
|
-
const { key } = msg.Res;
|
|
940
|
-
const existing = pairs.get(key);
|
|
941
|
-
const merged = {
|
|
942
|
-
request: existing?.request ?? {},
|
|
943
|
-
response: msg.Res
|
|
944
|
-
};
|
|
945
|
-
upsertPair(key, merged);
|
|
946
|
-
}
|
|
947
|
-
const reversedPairs = [];
|
|
948
|
-
for (let i = pairKeys.length - 1; i >= 0; i--) {
|
|
949
|
-
const key = pairKeys[i];
|
|
950
|
-
const pair = pairs.get(key);
|
|
951
|
-
if (pair) {
|
|
952
|
-
reversedPairs.push(pair);
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
onUpdate(reversedPairs);
|
|
956
|
-
} catch (err) {
|
|
957
|
-
logger.error("Error parsing WebSocket message:", err.message || err);
|
|
958
|
-
}
|
|
959
|
-
});
|
|
960
|
-
ws.on("close", () => {
|
|
961
|
-
logger.warn("Web debugger disconnected. Reconnecting in 5s...");
|
|
962
|
-
if (!isStopped) {
|
|
963
|
-
reconnectTimeout = setTimeout(connect, 5e3);
|
|
964
|
-
}
|
|
965
|
-
});
|
|
966
|
-
ws.on("error", (err) => {
|
|
967
|
-
logger.error(`WebSocket error: ${err.message}`);
|
|
968
|
-
});
|
|
969
|
-
};
|
|
970
|
-
connect();
|
|
971
|
-
return {
|
|
972
|
-
close: () => {
|
|
973
|
-
isStopped = true;
|
|
974
|
-
if (socket) {
|
|
975
|
-
socket.close();
|
|
976
|
-
}
|
|
977
|
-
if (reconnectTimeout) {
|
|
978
|
-
clearTimeout(reconnectTimeout);
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
};
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
// src/tui/blessed/components/UIComponents.ts
|
|
985
|
-
import blessed from "blessed";
|
|
986
|
-
|
|
987
|
-
// src/tui/ink/asciArt.ts
|
|
988
|
-
var asciiArtPinggyLogo = `
|
|
989
|
-
\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557
|
|
990
|
-
\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u255A\u2588\u2588\u2557 \u2588\u2588\u2554\u255D
|
|
991
|
-
\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2554\u255D
|
|
992
|
-
\u2588\u2588\u2554\u2550\u2550\u2550\u255D \u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u255A\u2588\u2588\u2554\u255D
|
|
993
|
-
\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551
|
|
994
|
-
\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D `;
|
|
995
|
-
|
|
996
|
-
// src/tui/blessed/components/UIComponents.ts
|
|
997
|
-
var MIN_WIDTH_WARNING = 60;
|
|
998
|
-
var SIMPLE_LAYOUT_THRESHOLD = 80;
|
|
999
|
-
function colorizeGradient(text) {
|
|
1000
|
-
const colors = ["red", "yellow", "green", "cyan", "blue", "magenta"];
|
|
1001
|
-
const lines = text.split("\n");
|
|
1002
|
-
return lines.map((line, i) => {
|
|
1003
|
-
const color = colors[i % colors.length];
|
|
1004
|
-
return `{${color}-fg}${line}{/${color}-fg}`;
|
|
1005
|
-
}).join("\n");
|
|
1006
|
-
}
|
|
1007
|
-
function createWarningUI(screen) {
|
|
1008
|
-
return blessed.box({
|
|
1009
|
-
parent: screen,
|
|
1010
|
-
top: "center",
|
|
1011
|
-
left: "center",
|
|
1012
|
-
width: "80%",
|
|
1013
|
-
height: 5,
|
|
1014
|
-
content: `{red-fg}{bold}Terminal is too narrow to show TUI (${screen.width} cols).{/bold}{/red-fg}
|
|
1015
|
-
{yellow-fg}Please resize your terminal to at least ${MIN_WIDTH_WARNING} columns for proper display.{/yellow-fg}`,
|
|
1016
|
-
tags: true,
|
|
1017
|
-
align: "center",
|
|
1018
|
-
valign: "middle",
|
|
1019
|
-
style: {
|
|
1020
|
-
fg: "red"
|
|
1021
|
-
}
|
|
1022
|
-
});
|
|
1023
|
-
}
|
|
1024
|
-
function createFullUI(screen, urls, greet, tunnelConfig) {
|
|
1025
|
-
const mainContainer = blessed.box({
|
|
1026
|
-
parent: screen,
|
|
1027
|
-
top: 0,
|
|
1028
|
-
left: 0,
|
|
1029
|
-
width: "100%",
|
|
1030
|
-
height: "100%",
|
|
1031
|
-
padding: 1
|
|
1032
|
-
});
|
|
1033
|
-
const logoBox = blessed.box({
|
|
1034
|
-
parent: mainContainer,
|
|
1035
|
-
top: 0,
|
|
1036
|
-
left: 0,
|
|
1037
|
-
width: "100%",
|
|
1038
|
-
height: 7,
|
|
1039
|
-
content: colorizeGradient(asciiArtPinggyLogo),
|
|
1040
|
-
tags: true
|
|
1041
|
-
});
|
|
1042
|
-
const contentBox = blessed.box({
|
|
1043
|
-
parent: mainContainer,
|
|
1044
|
-
top: 8,
|
|
1045
|
-
left: 0,
|
|
1046
|
-
width: "100%-2",
|
|
1047
|
-
height: "100%-10",
|
|
1048
|
-
padding: 0,
|
|
1049
|
-
border: {
|
|
1050
|
-
type: "line"
|
|
1051
|
-
},
|
|
1052
|
-
style: {
|
|
1053
|
-
border: {
|
|
1054
|
-
fg: "green"
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
});
|
|
1058
|
-
let greetHeight = 0;
|
|
1059
|
-
if (greet) {
|
|
1060
|
-
const greetBox = blessed.box({
|
|
1061
|
-
parent: contentBox,
|
|
1062
|
-
top: 0,
|
|
1063
|
-
left: "center",
|
|
1064
|
-
width: "60%",
|
|
1065
|
-
height: 4,
|
|
1066
|
-
content: `{bold}${greet}{/bold}`,
|
|
1067
|
-
tags: true,
|
|
1068
|
-
align: "center",
|
|
1069
|
-
style: {
|
|
1070
|
-
fg: "green"
|
|
1071
|
-
}
|
|
1072
|
-
});
|
|
1073
|
-
greetHeight = 4;
|
|
1074
|
-
}
|
|
1075
|
-
const upperSectionTop = greetHeight > 0 ? greetHeight : 0;
|
|
1076
|
-
const upperSection = blessed.box({
|
|
1077
|
-
parent: contentBox,
|
|
1078
|
-
top: upperSectionTop,
|
|
1079
|
-
left: 0,
|
|
1080
|
-
width: "100%-2",
|
|
1081
|
-
height: 10
|
|
1082
|
-
});
|
|
1083
|
-
const urlsBox = blessed.box({
|
|
1084
|
-
parent: upperSection,
|
|
1085
|
-
top: 0,
|
|
1086
|
-
left: 0,
|
|
1087
|
-
width: "48%",
|
|
1088
|
-
height: "100%",
|
|
1089
|
-
padding: { left: 1, right: 1 },
|
|
1090
|
-
tags: true
|
|
1091
|
-
});
|
|
1092
|
-
const statsBox = blessed.box({
|
|
1093
|
-
parent: upperSection,
|
|
1094
|
-
top: 0,
|
|
1095
|
-
right: 0,
|
|
1096
|
-
left: "65%",
|
|
1097
|
-
width: "35%",
|
|
1098
|
-
height: "100%",
|
|
1099
|
-
padding: { left: 1, right: 1 },
|
|
1100
|
-
tags: true,
|
|
1101
|
-
align: "left"
|
|
1102
|
-
});
|
|
1103
|
-
const lowerSectionTop = greetHeight + 11;
|
|
1104
|
-
const lowerSection = blessed.box({
|
|
1105
|
-
parent: contentBox,
|
|
1106
|
-
top: lowerSectionTop,
|
|
1107
|
-
left: 0,
|
|
1108
|
-
right: 0,
|
|
1109
|
-
bottom: 2,
|
|
1110
|
-
width: "100%-2",
|
|
1111
|
-
height: `100%-${lowerSectionTop + 6}`
|
|
1112
|
-
});
|
|
1113
|
-
const isQrCodeRequested = tunnelConfig?.isQRCode || false;
|
|
1114
|
-
const requestsBox = blessed.box({
|
|
1115
|
-
parent: lowerSection,
|
|
1116
|
-
top: 0,
|
|
1117
|
-
left: 0,
|
|
1118
|
-
width: isQrCodeRequested ? "60%" : "80%",
|
|
1119
|
-
height: "80%",
|
|
1120
|
-
padding: { left: 1, right: 1 },
|
|
1121
|
-
tags: true,
|
|
1122
|
-
scrollable: true
|
|
1123
|
-
});
|
|
1124
|
-
let qrCodeBox;
|
|
1125
|
-
if (isQrCodeRequested) {
|
|
1126
|
-
qrCodeBox = blessed.box({
|
|
1127
|
-
parent: lowerSection,
|
|
1128
|
-
top: 0,
|
|
1129
|
-
right: 0,
|
|
1130
|
-
width: "40%",
|
|
1131
|
-
height: "100%",
|
|
1132
|
-
tags: true,
|
|
1133
|
-
padding: { left: 1, right: 1 }
|
|
1134
|
-
});
|
|
1135
|
-
}
|
|
1136
|
-
const footerBox = blessed.box({
|
|
1137
|
-
parent: contentBox,
|
|
1138
|
-
bottom: 0,
|
|
1139
|
-
left: "center",
|
|
1140
|
-
width: "shrink",
|
|
1141
|
-
height: 1,
|
|
1142
|
-
content: "Press Ctrl+C to stop the tunnel. Or press h for key bindings.",
|
|
1143
|
-
tags: true
|
|
1144
|
-
});
|
|
1145
|
-
return {
|
|
1146
|
-
mainContainer,
|
|
1147
|
-
logoBox,
|
|
1148
|
-
contentBox,
|
|
1149
|
-
urlsBox,
|
|
1150
|
-
statsBox,
|
|
1151
|
-
requestsBox,
|
|
1152
|
-
qrCodeBox,
|
|
1153
|
-
footerBox
|
|
1154
|
-
};
|
|
1155
|
-
}
|
|
1156
|
-
function createSimpleUI(screen, urls, greet) {
|
|
1157
|
-
const mainContainer = blessed.box({
|
|
1158
|
-
parent: screen,
|
|
1159
|
-
top: 0,
|
|
1160
|
-
left: 0,
|
|
1161
|
-
width: "100%",
|
|
1162
|
-
height: "100%",
|
|
1163
|
-
padding: { left: 1, right: 1 }
|
|
1164
|
-
});
|
|
1165
|
-
let currentTop = 0;
|
|
1166
|
-
if (greet) {
|
|
1167
|
-
blessed.box({
|
|
1168
|
-
parent: mainContainer,
|
|
1169
|
-
top: currentTop,
|
|
1170
|
-
left: "center",
|
|
1171
|
-
width: "90%",
|
|
1172
|
-
height: "shrink",
|
|
1173
|
-
content: `{bold}${greet}{/bold}`,
|
|
1174
|
-
tags: true,
|
|
1175
|
-
align: "center",
|
|
1176
|
-
style: {
|
|
1177
|
-
fg: "green"
|
|
1178
|
-
}
|
|
1179
|
-
});
|
|
1180
|
-
const lines = Math.ceil(greet.length / (screen.width * 0.9));
|
|
1181
|
-
currentTop += Math.max(lines, 1) + 1;
|
|
1182
|
-
}
|
|
1183
|
-
const urlsBox = blessed.box({
|
|
1184
|
-
parent: mainContainer,
|
|
1185
|
-
top: currentTop,
|
|
1186
|
-
left: 0,
|
|
1187
|
-
width: "100%",
|
|
1188
|
-
height: urls.length + 2,
|
|
1189
|
-
tags: true
|
|
1190
|
-
});
|
|
1191
|
-
currentTop += urls.length + 3;
|
|
1192
|
-
const statsBox = blessed.box({
|
|
1193
|
-
parent: mainContainer,
|
|
1194
|
-
top: currentTop,
|
|
1195
|
-
left: 0,
|
|
1196
|
-
width: "100%",
|
|
1197
|
-
height: 8,
|
|
1198
|
-
tags: true
|
|
1199
|
-
});
|
|
1200
|
-
currentTop += 9;
|
|
1201
|
-
const footerBox = blessed.box({
|
|
1202
|
-
parent: mainContainer,
|
|
1203
|
-
bottom: 0,
|
|
1204
|
-
left: "center",
|
|
1205
|
-
width: "shrink",
|
|
1206
|
-
height: 1,
|
|
1207
|
-
content: "Press Ctrl+C to stop the tunnel.",
|
|
1208
|
-
tags: true,
|
|
1209
|
-
style: {
|
|
1210
|
-
fg: "white"
|
|
1211
|
-
}
|
|
1212
|
-
});
|
|
1213
|
-
return {
|
|
1214
|
-
mainContainer,
|
|
1215
|
-
urlsBox,
|
|
1216
|
-
statsBox,
|
|
1217
|
-
footerBox
|
|
1218
|
-
};
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
// src/tui/ink/utils/utils.ts
|
|
1222
|
-
function getStatusColor(status) {
|
|
1223
|
-
const match = status.match(/\b(\d{3})\b/);
|
|
1224
|
-
const statusCode = match ? parseInt(match[1], 10) : 0;
|
|
1225
|
-
switch (true) {
|
|
1226
|
-
case (statusCode >= 100 && statusCode < 200):
|
|
1227
|
-
return "yellow";
|
|
1228
|
-
case (statusCode >= 200 && statusCode < 300):
|
|
1229
|
-
return "green";
|
|
1230
|
-
case (statusCode >= 300 && statusCode < 400):
|
|
1231
|
-
return "yellow";
|
|
1232
|
-
case (statusCode >= 400 && statusCode < 500):
|
|
1233
|
-
return "red";
|
|
1234
|
-
case statusCode >= 500:
|
|
1235
|
-
return "pink";
|
|
1236
|
-
default:
|
|
1237
|
-
return "yellow";
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
function getBytesInt(b) {
|
|
1241
|
-
if (b >= 1024 * 1024 * 1024) {
|
|
1242
|
-
return `${(b / (1024 * 1024 * 1024)).toFixed(2)} G`;
|
|
1243
|
-
}
|
|
1244
|
-
if (b >= 1024 * 1024) {
|
|
1245
|
-
return `${(b / (1024 * 1024)).toFixed(2)} M`;
|
|
1246
|
-
}
|
|
1247
|
-
if (b >= 1024) {
|
|
1248
|
-
return `${(b / 1024).toFixed(2)} K`;
|
|
1249
|
-
}
|
|
1250
|
-
return `${b.toFixed(2)} `;
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
// src/tui/blessed/components/DisplayUpdaters.ts
|
|
1254
|
-
function updateUrlsDisplay(urlsBox, screen, urls, currentQrIndex) {
|
|
1255
|
-
if (!urlsBox) return;
|
|
1256
|
-
const config = getTuiConfig();
|
|
1257
|
-
const { visibleUrlCount } = config;
|
|
1258
|
-
let viewportStart = 0;
|
|
1259
|
-
if (urls.length > visibleUrlCount) {
|
|
1260
|
-
viewportStart = Math.max(0, Math.min(
|
|
1261
|
-
currentQrIndex - Math.floor(visibleUrlCount / 2),
|
|
1262
|
-
urls.length - visibleUrlCount
|
|
1263
|
-
));
|
|
1264
|
-
}
|
|
1265
|
-
const viewportEnd = Math.min(viewportStart + visibleUrlCount, urls.length);
|
|
1266
|
-
const visibleUrls = urls.slice(viewportStart, viewportEnd);
|
|
1267
|
-
let content = "{green-fg}{bold}Public URLs{/bold}{/green-fg}";
|
|
1268
|
-
if (viewportStart > 0) {
|
|
1269
|
-
content += ` {gray-fg}\u2191 ${viewportStart} more{/gray-fg}`;
|
|
1270
|
-
}
|
|
1271
|
-
content += "\n";
|
|
1272
|
-
visibleUrls.forEach((url, i) => {
|
|
1273
|
-
const index = viewportStart + i;
|
|
1274
|
-
const isSelected = index === currentQrIndex;
|
|
1275
|
-
const prefix = isSelected ? "\u2192 " : "\u2022 ";
|
|
1276
|
-
const color = isSelected ? "yellow" : "magenta";
|
|
1277
|
-
if (isSelected) {
|
|
1278
|
-
content += `{bold}{${color}-fg}${prefix}${url}{/${color}-fg}{/bold}
|
|
1279
|
-
`;
|
|
1280
|
-
} else {
|
|
1281
|
-
content += `{${color}-fg}${prefix}${url}{/${color}-fg}
|
|
1282
|
-
`;
|
|
1283
|
-
}
|
|
1284
|
-
});
|
|
1285
|
-
const itemsBelow = urls.length - viewportEnd;
|
|
1286
|
-
if (itemsBelow > 0) {
|
|
1287
|
-
content += `{gray-fg}\u2193 ${itemsBelow} more{/gray-fg}
|
|
1288
|
-
`;
|
|
1289
|
-
}
|
|
1290
|
-
urlsBox.setContent(content);
|
|
1291
|
-
screen.render();
|
|
1292
|
-
}
|
|
1293
|
-
function updateStatsDisplay(statsBox, screen, stats) {
|
|
1294
|
-
if (!statsBox) return;
|
|
1295
|
-
const content = `{green-fg}{bold}Live Stats{/bold}{/green-fg}
|
|
1296
|
-
Elapsed: ${stats.elapsedTime}s
|
|
1297
|
-
Live Connections: ${stats.numLiveConnections}
|
|
1298
|
-
Total Connections: ${stats.numTotalConnections}
|
|
1299
|
-
Request: ${getBytesInt(stats.numTotalReqBytes)}
|
|
1300
|
-
Response: ${getBytesInt(stats.numTotalResBytes)}
|
|
1301
|
-
Total Transfer: ${getBytesInt(stats.numTotalTxBytes)}`;
|
|
1302
|
-
statsBox.setContent(content);
|
|
1303
|
-
statsBox.style = { ...statsBox.style };
|
|
1304
|
-
statsBox.parseContent();
|
|
1305
|
-
screen.render();
|
|
1306
|
-
}
|
|
1307
|
-
function updateRequestsDisplay(requestsBox, screen, pairs, selectedIndex) {
|
|
1308
|
-
const config = getTuiConfig();
|
|
1309
|
-
const { maxRequestPairs, visibleRequestCount, viewportScrollMargin } = config;
|
|
1310
|
-
if (!requestsBox) {
|
|
1311
|
-
return { adjustedSelectedIndex: selectedIndex, trimmedPairs: pairs };
|
|
1312
|
-
}
|
|
1313
|
-
let allPairs = pairs;
|
|
1314
|
-
let trimmedPairs = pairs;
|
|
1315
|
-
if (allPairs.length > maxRequestPairs) {
|
|
1316
|
-
allPairs = allPairs.slice(0, maxRequestPairs);
|
|
1317
|
-
trimmedPairs = allPairs;
|
|
1318
|
-
}
|
|
1319
|
-
const totalPairs = allPairs.length;
|
|
1320
|
-
let adjustedSelectedIndex = selectedIndex;
|
|
1321
|
-
if (adjustedSelectedIndex >= totalPairs) {
|
|
1322
|
-
adjustedSelectedIndex = -1;
|
|
1323
|
-
}
|
|
1324
|
-
let viewportStart;
|
|
1325
|
-
if (totalPairs <= visibleRequestCount) {
|
|
1326
|
-
viewportStart = 0;
|
|
1327
|
-
} else if (adjustedSelectedIndex === -1) {
|
|
1328
|
-
viewportStart = 0;
|
|
1329
|
-
} else {
|
|
1330
|
-
viewportStart = 0;
|
|
1331
|
-
if (adjustedSelectedIndex >= visibleRequestCount - viewportScrollMargin) {
|
|
1332
|
-
viewportStart = Math.min(
|
|
1333
|
-
totalPairs - visibleRequestCount,
|
|
1334
|
-
adjustedSelectedIndex - viewportScrollMargin
|
|
1335
|
-
);
|
|
1336
|
-
}
|
|
1337
|
-
if (adjustedSelectedIndex < viewportStart + viewportScrollMargin) {
|
|
1338
|
-
viewportStart = Math.max(0, adjustedSelectedIndex - viewportScrollMargin);
|
|
1339
|
-
}
|
|
1340
|
-
}
|
|
1341
|
-
const viewportEnd = Math.min(viewportStart + visibleRequestCount, totalPairs);
|
|
1342
|
-
const visiblePairs = allPairs.slice(viewportStart, viewportEnd);
|
|
1343
|
-
let content = "{yellow-fg}HTTP Requests:{/yellow-fg}";
|
|
1344
|
-
if (viewportStart > 0) {
|
|
1345
|
-
content += ` {gray-fg}\u2191 ${viewportStart} more{/gray-fg}`;
|
|
1346
|
-
}
|
|
1347
|
-
content += "\n";
|
|
1348
|
-
visiblePairs.forEach((pair, i) => {
|
|
1349
|
-
const globalIndex = viewportStart + i;
|
|
1350
|
-
const isSelected = adjustedSelectedIndex !== -1 && adjustedSelectedIndex === globalIndex;
|
|
1351
|
-
const prefix = isSelected ? "> " : " ";
|
|
1352
|
-
const method = pair.request?.method || "";
|
|
1353
|
-
const uri = pair.request?.uri || "";
|
|
1354
|
-
const status = pair.response?.status || "";
|
|
1355
|
-
const statusColor = getStatusColor(String(status));
|
|
1356
|
-
if (isSelected) {
|
|
1357
|
-
content += `{cyan-fg}${prefix}${method} ${status} ${uri}{/cyan-fg}
|
|
1358
|
-
`;
|
|
1359
|
-
} else if (pair.response) {
|
|
1360
|
-
content += `{${statusColor}-fg}${prefix}${method} ${status} ${uri}{/${statusColor}-fg}
|
|
1361
|
-
`;
|
|
1362
|
-
} else {
|
|
1363
|
-
content += `${prefix}${method} ...${uri}
|
|
1364
|
-
`;
|
|
1365
|
-
}
|
|
1366
|
-
});
|
|
1367
|
-
const itemsBelow = totalPairs - viewportEnd;
|
|
1368
|
-
if (itemsBelow > 0) {
|
|
1369
|
-
content += `{gray-fg} \u2193 ${itemsBelow} more{/gray-fg}
|
|
1370
|
-
`;
|
|
1371
|
-
}
|
|
1372
|
-
requestsBox.setContent(content);
|
|
1373
|
-
screen.render();
|
|
1374
|
-
return { adjustedSelectedIndex, trimmedPairs };
|
|
1375
|
-
}
|
|
1376
|
-
function updateQrCodeDisplay(qrCodeBox, screen, qrCodes, urls, currentQrIndex) {
|
|
1377
|
-
if (!qrCodeBox || qrCodes.length === 0) return;
|
|
1378
|
-
let content = `{green-fg}{bold}QR Code ${currentQrIndex + 1}/${urls.length}{/bold}{/green-fg}
|
|
1379
|
-
`;
|
|
1380
|
-
if (urls.length > 1) {
|
|
1381
|
-
content += "\n{yellow-fg}\u2190 \u2192 to switch QR codes{/yellow-fg}\n";
|
|
1382
|
-
}
|
|
1383
|
-
content += qrCodes[currentQrIndex] || "";
|
|
1384
|
-
qrCodeBox.setContent(content);
|
|
1385
|
-
qrCodeBox.style = { ...qrCodeBox.style };
|
|
1386
|
-
qrCodeBox.parseContent();
|
|
1387
|
-
screen.render();
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
// src/tui/blessed/components/Modals.ts
|
|
1391
|
-
import blessed2 from "blessed";
|
|
1392
|
-
function showDetailModal(screen, manager, requestText, responseText) {
|
|
1393
|
-
manager.inDetailView = true;
|
|
1394
|
-
manager.detailModal = blessed2.box({
|
|
1395
|
-
parent: screen,
|
|
1396
|
-
top: "center",
|
|
1397
|
-
left: "center",
|
|
1398
|
-
width: "90%",
|
|
1399
|
-
height: "90%",
|
|
1400
|
-
border: {
|
|
1401
|
-
type: "line"
|
|
1402
|
-
},
|
|
1403
|
-
style: {
|
|
1404
|
-
border: {
|
|
1405
|
-
fg: "green"
|
|
1406
|
-
}
|
|
1407
|
-
},
|
|
1408
|
-
padding: { left: 2, right: 2, top: 1, bottom: 1 },
|
|
1409
|
-
tags: true,
|
|
1410
|
-
scrollable: true,
|
|
1411
|
-
keys: true,
|
|
1412
|
-
vi: true,
|
|
1413
|
-
alwaysScroll: true,
|
|
1414
|
-
scrollbar: {
|
|
1415
|
-
ch: " ",
|
|
1416
|
-
track: {
|
|
1417
|
-
bg: "cyan"
|
|
1418
|
-
},
|
|
1419
|
-
style: {
|
|
1420
|
-
inverse: true
|
|
1421
|
-
}
|
|
1422
|
-
}
|
|
1423
|
-
});
|
|
1424
|
-
const content = `{cyan-fg}{bold}Request{/bold}{/cyan-fg}
|
|
1425
|
-
${requestText || "(no request data)"}
|
|
1426
|
-
|
|
1427
|
-
{magenta-fg}{bold}Response{/bold}{/magenta-fg}
|
|
1428
|
-
${responseText || "(no response data)"}
|
|
1429
|
-
|
|
1430
|
-
{white-bg}{black-fg}Press ESC to close{/black-fg}{/white-bg}`;
|
|
1431
|
-
manager.detailModal.setContent(content);
|
|
1432
|
-
manager.detailModal.focus();
|
|
1433
|
-
screen.render();
|
|
1434
|
-
}
|
|
1435
|
-
function closeDetailModal(screen, manager) {
|
|
1436
|
-
if (manager.detailModal) {
|
|
1437
|
-
manager.detailModal.destroy();
|
|
1438
|
-
manager.detailModal = null;
|
|
1439
|
-
}
|
|
1440
|
-
manager.inDetailView = false;
|
|
1441
|
-
screen.render();
|
|
1442
|
-
}
|
|
1443
|
-
function showKeyBindingsModal(screen, manager) {
|
|
1444
|
-
manager.keyBindingView = true;
|
|
1445
|
-
manager.keyBindingsModal = blessed2.box({
|
|
1446
|
-
parent: screen,
|
|
1447
|
-
top: "center",
|
|
1448
|
-
left: "center",
|
|
1449
|
-
width: "60%",
|
|
1450
|
-
height: "80%",
|
|
1451
|
-
border: {
|
|
1452
|
-
type: "line"
|
|
1453
|
-
},
|
|
1454
|
-
style: {
|
|
1455
|
-
border: {
|
|
1456
|
-
fg: "green"
|
|
1457
|
-
}
|
|
1458
|
-
},
|
|
1459
|
-
padding: { left: 2, right: 2, top: 1, bottom: 1 },
|
|
1460
|
-
tags: true
|
|
1461
|
-
});
|
|
1462
|
-
const content = `{cyan-fg}{bold}Key Bindings{/bold}{/cyan-fg}
|
|
1463
|
-
|
|
1464
|
-
{bold}h{/bold} This page
|
|
1465
|
-
{bold}c{/bold} Copy the selected URL to clipboard
|
|
1466
|
-
{bold}Ctrl+c{/bold} Exit
|
|
1467
|
-
|
|
1468
|
-
Enter/Return Open selected request
|
|
1469
|
-
Esc Return to main page (or close modals)
|
|
1470
|
-
UP (\u2191) Scroll up the requests
|
|
1471
|
-
Down (\u2193) Scroll down the requests
|
|
1472
|
-
Left (\u2190) Show qr code for previous url
|
|
1473
|
-
Right (\u2192) Show qr code for next url
|
|
1474
|
-
Home Jump to top of requests
|
|
1475
|
-
End Jump to bottom of requests
|
|
1476
|
-
Ctrl+c Force Exit
|
|
1477
|
-
|
|
1478
|
-
{white-bg}{black-fg}Press ESC to close{/black-fg}{/white-bg}`;
|
|
1479
|
-
manager.keyBindingsModal.setContent(content);
|
|
1480
|
-
manager.keyBindingsModal.focus();
|
|
1481
|
-
screen.render();
|
|
1482
|
-
}
|
|
1483
|
-
function closeKeyBindingsModal(screen, manager) {
|
|
1484
|
-
if (manager.keyBindingsModal) {
|
|
1485
|
-
manager.keyBindingsModal.destroy();
|
|
1486
|
-
manager.keyBindingsModal = null;
|
|
1487
|
-
}
|
|
1488
|
-
manager.keyBindingView = false;
|
|
1489
|
-
screen.render();
|
|
1490
|
-
}
|
|
1491
|
-
function showDisconnectModal(screen, manager, message, onClose) {
|
|
1492
|
-
manager.inDisconnectView = true;
|
|
1493
|
-
manager.disconnectModal = blessed2.box({
|
|
1494
|
-
parent: screen,
|
|
1495
|
-
top: "center",
|
|
1496
|
-
left: "center",
|
|
1497
|
-
width: "50%",
|
|
1498
|
-
height: "20%",
|
|
1499
|
-
border: {
|
|
1500
|
-
type: "line"
|
|
1501
|
-
},
|
|
1502
|
-
style: {
|
|
1503
|
-
border: {
|
|
1504
|
-
fg: "red"
|
|
1505
|
-
}
|
|
1506
|
-
},
|
|
1507
|
-
padding: { left: 2, right: 2, top: 1, bottom: 1 },
|
|
1508
|
-
tags: true,
|
|
1509
|
-
align: "center",
|
|
1510
|
-
valign: "middle"
|
|
1511
|
-
});
|
|
1512
|
-
const content = `{red-fg}{bold}Tunnel Disconnected{/bold}{/red-fg}
|
|
1513
|
-
|
|
1514
|
-
${message || "Disconnect request received. Tunnel will be closed."}
|
|
1515
|
-
|
|
1516
|
-
{white-bg}{black-fg}Closing in 3 seconds... {/black-fg}{/white-bg}`;
|
|
1517
|
-
manager.disconnectModal.setContent(content);
|
|
1518
|
-
manager.disconnectModal.focus();
|
|
1519
|
-
screen.render();
|
|
1520
|
-
const timeout = setTimeout(() => {
|
|
1521
|
-
closeDisconnectModal(screen, manager);
|
|
1522
|
-
if (onClose) onClose();
|
|
1523
|
-
}, 5e3);
|
|
1524
|
-
const keyHandler = () => {
|
|
1525
|
-
clearTimeout(timeout);
|
|
1526
|
-
closeDisconnectModal(screen, manager);
|
|
1527
|
-
if (onClose) onClose();
|
|
1528
|
-
};
|
|
1529
|
-
manager.disconnectModal.key(["escape", "enter", "space"], keyHandler);
|
|
1530
|
-
screen.key(["escape", "enter", "space"], keyHandler);
|
|
1531
|
-
}
|
|
1532
|
-
function closeDisconnectModal(screen, manager) {
|
|
1533
|
-
if (manager.disconnectModal) {
|
|
1534
|
-
manager.disconnectModal.destroy();
|
|
1535
|
-
manager.disconnectModal = null;
|
|
1536
|
-
}
|
|
1537
|
-
manager.inDisconnectView = false;
|
|
1538
|
-
screen.render();
|
|
1539
|
-
}
|
|
1540
|
-
function showReconnectingModal(screen, manager, retryCnt, message) {
|
|
1541
|
-
if (manager.reconnectModal) {
|
|
1542
|
-
manager.reconnectModal.destroy();
|
|
1543
|
-
manager.reconnectModal = null;
|
|
1544
|
-
}
|
|
1545
|
-
manager.inReconnectView = true;
|
|
1546
|
-
manager.reconnectModal = blessed2.box({
|
|
1547
|
-
parent: screen,
|
|
1548
|
-
top: "center",
|
|
1549
|
-
left: "center",
|
|
1550
|
-
width: "50%",
|
|
1551
|
-
height: "20%",
|
|
1552
|
-
border: {
|
|
1553
|
-
type: "line"
|
|
1554
|
-
},
|
|
1555
|
-
style: {
|
|
1556
|
-
border: {
|
|
1557
|
-
fg: "yellow"
|
|
1558
|
-
}
|
|
1559
|
-
},
|
|
1560
|
-
padding: { left: 2, right: 2, top: 1, bottom: 1 },
|
|
1561
|
-
tags: true,
|
|
1562
|
-
align: "center",
|
|
1563
|
-
valign: "middle"
|
|
1564
|
-
});
|
|
1565
|
-
const content = `{yellow-fg}{bold}Reconnecting...{/bold}{/yellow-fg}
|
|
1566
|
-
|
|
1567
|
-
${message || `Attempt #${retryCnt} \u2014 trying to re-establish tunnel...`}
|
|
1568
|
-
|
|
1569
|
-
{gray-fg}Please wait{/gray-fg}`;
|
|
1570
|
-
manager.reconnectModal.setContent(content);
|
|
1571
|
-
manager.reconnectModal.focus();
|
|
1572
|
-
screen.render();
|
|
1573
|
-
}
|
|
1574
|
-
function closeReconnectingModal(screen, manager) {
|
|
1575
|
-
if (manager.reconnectModal) {
|
|
1576
|
-
manager.reconnectModal.destroy();
|
|
1577
|
-
manager.reconnectModal = null;
|
|
1578
|
-
}
|
|
1579
|
-
manager.inReconnectView = false;
|
|
1580
|
-
screen.render();
|
|
1581
|
-
}
|
|
1582
|
-
function showReconnectionFailedModal(screen, manager, retryCnt, onClose) {
|
|
1583
|
-
closeReconnectingModal(screen, manager);
|
|
1584
|
-
manager.inReconnectView = true;
|
|
1585
|
-
manager.reconnectModal = blessed2.box({
|
|
1586
|
-
parent: screen,
|
|
1587
|
-
top: "center",
|
|
1588
|
-
left: "center",
|
|
1589
|
-
width: "50%",
|
|
1590
|
-
height: "20%",
|
|
1591
|
-
border: {
|
|
1592
|
-
type: "line"
|
|
1593
|
-
},
|
|
1594
|
-
style: {
|
|
1595
|
-
border: {
|
|
1596
|
-
fg: "red"
|
|
1597
|
-
}
|
|
1598
|
-
},
|
|
1599
|
-
padding: { left: 2, right: 2, top: 1, bottom: 1 },
|
|
1600
|
-
tags: true,
|
|
1601
|
-
align: "center",
|
|
1602
|
-
valign: "middle"
|
|
1603
|
-
});
|
|
1604
|
-
const content = `{red-fg}{bold}Reconnection Failed{/bold}{/red-fg}
|
|
1605
|
-
|
|
1606
|
-
Failed to reconnect after ${retryCnt} attempts.
|
|
1607
|
-
Tunnel will be closed.
|
|
1608
|
-
|
|
1609
|
-
{white-bg}{black-fg}Closing in 5 seconds...{/black-fg}{/white-bg}`;
|
|
1610
|
-
manager.reconnectModal.setContent(content);
|
|
1611
|
-
manager.reconnectModal.focus();
|
|
1612
|
-
screen.render();
|
|
1613
|
-
const timeout = setTimeout(() => {
|
|
1614
|
-
closeReconnectingModal(screen, manager);
|
|
1615
|
-
if (onClose) onClose();
|
|
1616
|
-
}, 5e3);
|
|
1617
|
-
const keyHandler = () => {
|
|
1618
|
-
clearTimeout(timeout);
|
|
1619
|
-
closeReconnectingModal(screen, manager);
|
|
1620
|
-
if (onClose) onClose();
|
|
1621
|
-
};
|
|
1622
|
-
manager.reconnectModal.key(["escape", "enter", "space"], keyHandler);
|
|
1623
|
-
screen.key(["escape", "enter", "space"], keyHandler);
|
|
1624
|
-
}
|
|
1625
|
-
function showLoadingModal(screen, modalManager, message = "Loading...") {
|
|
1626
|
-
if (modalManager.loadingView) return;
|
|
1627
|
-
modalManager.loadingBox = blessed2.box({
|
|
1628
|
-
parent: screen,
|
|
1629
|
-
top: "center",
|
|
1630
|
-
left: "center",
|
|
1631
|
-
width: "60%",
|
|
1632
|
-
height: 8,
|
|
1633
|
-
border: { type: "line" },
|
|
1634
|
-
style: {
|
|
1635
|
-
border: { fg: "yellow" }
|
|
1636
|
-
},
|
|
1637
|
-
tags: true,
|
|
1638
|
-
content: `{center}{yellow-fg}{bold}${message}{/bold}{/yellow-fg}
|
|
1639
|
-
|
|
1640
|
-
{gray-fg}Press ESC to cancel{/gray-fg}{/center}`,
|
|
1641
|
-
valign: "middle"
|
|
1642
|
-
});
|
|
1643
|
-
modalManager.loadingView = true;
|
|
1644
|
-
screen.render();
|
|
1645
|
-
}
|
|
1646
|
-
function closeLoadingModal(screen, modalManager) {
|
|
1647
|
-
if (!modalManager.loadingView || !modalManager.loadingBox) return;
|
|
1648
|
-
modalManager.loadingBox.destroy();
|
|
1649
|
-
modalManager.loadingBox = null;
|
|
1650
|
-
modalManager.loadingView = false;
|
|
1651
|
-
screen.render();
|
|
1652
|
-
}
|
|
1653
|
-
function showErrorModal(screen, modalManager, title = "Error", message) {
|
|
1654
|
-
if (modalManager.loadingBox) {
|
|
1655
|
-
modalManager.loadingBox.destroy();
|
|
1656
|
-
modalManager.loadingBox = null;
|
|
1657
|
-
}
|
|
1658
|
-
modalManager.loadingBox = blessed2.box({
|
|
1659
|
-
parent: screen,
|
|
1660
|
-
top: "center",
|
|
1661
|
-
left: "center",
|
|
1662
|
-
width: "60%",
|
|
1663
|
-
height: 9,
|
|
1664
|
-
border: { type: "line" },
|
|
1665
|
-
style: {
|
|
1666
|
-
border: { fg: "red" }
|
|
1667
|
-
},
|
|
1668
|
-
tags: true,
|
|
1669
|
-
content: `{center}{red-fg}{bold}${title}{/bold}{/red-fg}
|
|
1670
|
-
|
|
1671
|
-
{white-fg}${message}{/white-fg}
|
|
1672
|
-
|
|
1673
|
-
{gray-fg}Press ESC to close{/gray-fg}{/center}`,
|
|
1674
|
-
valign: "middle"
|
|
1675
|
-
});
|
|
1676
|
-
modalManager.loadingView = true;
|
|
1677
|
-
screen.render();
|
|
1678
|
-
}
|
|
1679
|
-
|
|
1680
|
-
// src/tui/blessed/headerFetcher.ts
|
|
1681
|
-
async function fetchReqResHeaders(baseUrl, key, signal) {
|
|
1682
|
-
if (!baseUrl) {
|
|
1683
|
-
return { req: "", res: "" };
|
|
1684
|
-
}
|
|
1685
|
-
try {
|
|
1686
|
-
const [reqRes, resRes] = await Promise.all([
|
|
1687
|
-
fetch(`http://${baseUrl}/introspec/getrawrequestheader`, {
|
|
1688
|
-
headers: { "X-Introspec-Key": key.toString() },
|
|
1689
|
-
signal
|
|
1690
|
-
}),
|
|
1691
|
-
fetch(`http://${baseUrl}/introspec/getrawresponseheader`, {
|
|
1692
|
-
headers: { "X-Introspec-Key": key.toString() },
|
|
1693
|
-
signal
|
|
1694
|
-
})
|
|
1695
|
-
]);
|
|
1696
|
-
const [req, res] = await Promise.all([reqRes.text(), resRes.text()]);
|
|
1697
|
-
return { req, res };
|
|
1698
|
-
} catch (err) {
|
|
1699
|
-
if (err?.name === "AbortError") {
|
|
1700
|
-
throw err;
|
|
1701
|
-
}
|
|
1702
|
-
logger.error("Error fetching headers:", err.message || err);
|
|
1703
|
-
throw err;
|
|
1704
|
-
}
|
|
1705
|
-
}
|
|
1706
|
-
|
|
1707
|
-
// src/tui/blessed/components/KeyBindings.ts
|
|
1708
|
-
function setupKeyBindings(screen, modalManager, state, callbacks, tunnelConfig) {
|
|
1709
|
-
let inactivityTimeout = null;
|
|
1710
|
-
const { inactivityHttpSelectorTimeoutMs } = getTuiConfig();
|
|
1711
|
-
const INACTIVITY_TIMEOUT_MS = inactivityHttpSelectorTimeoutMs;
|
|
1712
|
-
const resetInactivityTimer = () => {
|
|
1713
|
-
if (inactivityTimeout) {
|
|
1714
|
-
clearTimeout(inactivityTimeout);
|
|
1715
|
-
}
|
|
1716
|
-
if (state.selectedIndex !== -1) {
|
|
1717
|
-
inactivityTimeout = setTimeout(() => {
|
|
1718
|
-
callbacks.onSelectedIndexChange(-1, null);
|
|
1719
|
-
callbacks.updateRequestsDisplay();
|
|
1720
|
-
}, INACTIVITY_TIMEOUT_MS);
|
|
1721
|
-
}
|
|
1722
|
-
};
|
|
1723
|
-
screen.key(["C-c"], () => {
|
|
1724
|
-
callbacks.onDestroy();
|
|
1725
|
-
process.exit(0);
|
|
1726
|
-
});
|
|
1727
|
-
screen.key(["escape"], () => {
|
|
1728
|
-
if (modalManager.loadingView) {
|
|
1729
|
-
if (modalManager.fetchAbortController) {
|
|
1730
|
-
modalManager.fetchAbortController.abort();
|
|
1731
|
-
modalManager.fetchAbortController = null;
|
|
1732
|
-
}
|
|
1733
|
-
closeLoadingModal(screen, modalManager);
|
|
1734
|
-
return;
|
|
1735
|
-
}
|
|
1736
|
-
if (modalManager.inDetailView) {
|
|
1737
|
-
closeDetailModal(screen, modalManager);
|
|
1738
|
-
return;
|
|
1739
|
-
}
|
|
1740
|
-
if (modalManager.keyBindingView) {
|
|
1741
|
-
closeKeyBindingsModal(screen, modalManager);
|
|
1742
|
-
return;
|
|
1743
|
-
}
|
|
1744
|
-
if (state.selectedIndex !== -1) {
|
|
1745
|
-
if (inactivityTimeout) {
|
|
1746
|
-
clearTimeout(inactivityTimeout);
|
|
1747
|
-
inactivityTimeout = null;
|
|
1748
|
-
}
|
|
1749
|
-
callbacks.onSelectedIndexChange(-1, null);
|
|
1750
|
-
callbacks.updateRequestsDisplay();
|
|
1751
|
-
}
|
|
1752
|
-
});
|
|
1753
|
-
screen.key(["up"], () => {
|
|
1754
|
-
if (modalManager.inDetailView || modalManager.keyBindingView || modalManager.loadingView) return;
|
|
1755
|
-
resetInactivityTimer();
|
|
1756
|
-
if (state.selectedIndex === -1) {
|
|
1757
|
-
const requestKey = state.pairs[0]?.request?.key ?? null;
|
|
1758
|
-
callbacks.onSelectedIndexChange(0, requestKey);
|
|
1759
|
-
callbacks.updateRequestsDisplay();
|
|
1760
|
-
resetInactivityTimer();
|
|
1761
|
-
} else if (state.selectedIndex > 0) {
|
|
1762
|
-
const newIndex = state.selectedIndex - 1;
|
|
1763
|
-
const requestKey = state.pairs[newIndex]?.request?.key ?? null;
|
|
1764
|
-
callbacks.onSelectedIndexChange(newIndex, requestKey);
|
|
1765
|
-
callbacks.updateRequestsDisplay();
|
|
1766
|
-
}
|
|
1767
|
-
});
|
|
1768
|
-
screen.key(["down"], () => {
|
|
1769
|
-
if (modalManager.inDetailView || modalManager.keyBindingView || modalManager.loadingView) return;
|
|
1770
|
-
resetInactivityTimer();
|
|
1771
|
-
const config = getTuiConfig();
|
|
1772
|
-
const limitedLength = Math.min(state.pairs.length, config.maxRequestPairs);
|
|
1773
|
-
if (state.selectedIndex === -1) {
|
|
1774
|
-
if (limitedLength > 0) {
|
|
1775
|
-
const requestKey = state.pairs[0]?.request?.key ?? null;
|
|
1776
|
-
callbacks.onSelectedIndexChange(0, requestKey);
|
|
1777
|
-
callbacks.updateRequestsDisplay();
|
|
1778
|
-
resetInactivityTimer();
|
|
1779
|
-
}
|
|
1780
|
-
} else if (state.selectedIndex < limitedLength - 1) {
|
|
1781
|
-
const newIndex = state.selectedIndex + 1;
|
|
1782
|
-
const requestKey = state.pairs[newIndex]?.request?.key ?? null;
|
|
1783
|
-
callbacks.onSelectedIndexChange(newIndex, requestKey);
|
|
1784
|
-
callbacks.updateRequestsDisplay();
|
|
1785
|
-
}
|
|
1786
|
-
});
|
|
1787
|
-
screen.key(["end"], () => {
|
|
1788
|
-
if (modalManager.inDetailView || modalManager.keyBindingView || modalManager.loadingView) return;
|
|
1789
|
-
resetInactivityTimer();
|
|
1790
|
-
const config = getTuiConfig();
|
|
1791
|
-
const limitedLength = Math.min(state.pairs.length, config.maxRequestPairs);
|
|
1792
|
-
const lastIndex = Math.max(0, limitedLength - 1);
|
|
1793
|
-
if (state.selectedIndex !== lastIndex) {
|
|
1794
|
-
const requestKey = state.pairs[lastIndex]?.request?.key ?? null;
|
|
1795
|
-
callbacks.onSelectedIndexChange(lastIndex, requestKey);
|
|
1796
|
-
callbacks.updateRequestsDisplay();
|
|
1797
|
-
}
|
|
1798
|
-
});
|
|
1799
|
-
screen.key(["enter"], async () => {
|
|
1800
|
-
if (modalManager.inDetailView || modalManager.keyBindingView || modalManager.loadingView) return;
|
|
1801
|
-
if (state.selectedIndex === -1) return;
|
|
1802
|
-
resetInactivityTimer();
|
|
1803
|
-
const pair = state.pairs[state.selectedIndex];
|
|
1804
|
-
if (pair?.request?.key !== void 0 && pair?.request?.key !== null) {
|
|
1805
|
-
const abortController = new AbortController();
|
|
1806
|
-
modalManager.fetchAbortController = abortController;
|
|
1807
|
-
showLoadingModal(screen, modalManager, "Fetching request details...");
|
|
1808
|
-
try {
|
|
1809
|
-
const headers = await fetchReqResHeaders(
|
|
1810
|
-
tunnelConfig?.webDebugger || "",
|
|
1811
|
-
pair.request.key,
|
|
1812
|
-
abortController.signal
|
|
1813
|
-
);
|
|
1814
|
-
if (abortController.signal.aborted) {
|
|
1815
|
-
return;
|
|
1816
|
-
}
|
|
1817
|
-
closeLoadingModal(screen, modalManager);
|
|
1818
|
-
modalManager.fetchAbortController = null;
|
|
1819
|
-
showDetailModal(screen, modalManager, headers.req, headers.res);
|
|
1820
|
-
} catch (err) {
|
|
1821
|
-
if (err?.name === "AbortError" || abortController.signal.aborted) {
|
|
1822
|
-
logger.info("Fetch request cancelled by user");
|
|
1823
|
-
return;
|
|
1824
|
-
}
|
|
1825
|
-
closeLoadingModal(screen, modalManager);
|
|
1826
|
-
modalManager.fetchAbortController = null;
|
|
1827
|
-
const errorMessage = err?.message || String(err) || "Unknown error occurred";
|
|
1828
|
-
logger.error("Fetch error:", err);
|
|
1829
|
-
showErrorModal(screen, modalManager, "Failed to fetch request details", errorMessage);
|
|
1830
|
-
}
|
|
1831
|
-
}
|
|
1832
|
-
});
|
|
1833
|
-
screen.key(["h"], () => {
|
|
1834
|
-
if (modalManager.inDetailView || modalManager.loadingView) return;
|
|
1835
|
-
if (modalManager.keyBindingView) {
|
|
1836
|
-
closeKeyBindingsModal(screen, modalManager);
|
|
1837
|
-
} else {
|
|
1838
|
-
showKeyBindingsModal(screen, modalManager);
|
|
1839
|
-
}
|
|
1840
|
-
});
|
|
1841
|
-
screen.key(["c"], async () => {
|
|
1842
|
-
if (modalManager.inDetailView || modalManager.keyBindingView || modalManager.loadingView) return;
|
|
1843
|
-
if (state.urls.length > 0) {
|
|
1844
|
-
try {
|
|
1845
|
-
const clipboardy = await import("clipboardy");
|
|
1846
|
-
clipboardy.default.writeSync(state.urls[state.currentQrIndex]);
|
|
1847
|
-
} catch (err) {
|
|
1848
|
-
logger.error("Failed to copy to clipboard:", err);
|
|
1849
|
-
}
|
|
1850
|
-
}
|
|
1851
|
-
});
|
|
1852
|
-
screen.key(["left"], () => {
|
|
1853
|
-
if (modalManager.inDetailView || modalManager.keyBindingView || modalManager.loadingView) return;
|
|
1854
|
-
if (state.currentQrIndex > 0) {
|
|
1855
|
-
callbacks.onQrIndexChange(state.currentQrIndex - 1);
|
|
1856
|
-
callbacks.updateUrlsDisplay();
|
|
1857
|
-
callbacks.updateQrCodeDisplay();
|
|
1858
|
-
}
|
|
1859
|
-
});
|
|
1860
|
-
screen.key(["right"], () => {
|
|
1861
|
-
if (modalManager.inDetailView || modalManager.keyBindingView || modalManager.loadingView) return;
|
|
1862
|
-
if (state.currentQrIndex < state.urls.length - 1) {
|
|
1863
|
-
callbacks.onQrIndexChange(state.currentQrIndex + 1);
|
|
1864
|
-
callbacks.updateUrlsDisplay();
|
|
1865
|
-
callbacks.updateQrCodeDisplay();
|
|
1866
|
-
}
|
|
1867
|
-
});
|
|
1868
|
-
}
|
|
1869
|
-
|
|
1870
|
-
// src/tui/blessed/TunnelTui.ts
|
|
1871
|
-
var TunnelTui = class {
|
|
1872
|
-
constructor(props) {
|
|
1873
|
-
// State
|
|
1874
|
-
this.currentQrIndex = 0;
|
|
1875
|
-
this.selectedIndex = -1;
|
|
1876
|
-
// -1 means no selection
|
|
1877
|
-
this.selectedRequestKey = null;
|
|
1878
|
-
// Track selected request by key
|
|
1879
|
-
this.qrCodes = [];
|
|
1880
|
-
this.stats = {
|
|
1881
|
-
elapsedTime: 0,
|
|
1882
|
-
numLiveConnections: 0,
|
|
1883
|
-
numTotalConnections: 0,
|
|
1884
|
-
numTotalReqBytes: 0,
|
|
1885
|
-
numTotalResBytes: 0,
|
|
1886
|
-
numTotalTxBytes: 0
|
|
1887
|
-
};
|
|
1888
|
-
this.pairs = [];
|
|
1889
|
-
this.webDebuggerConnection = null;
|
|
1890
|
-
this.modalManager = {
|
|
1891
|
-
detailModal: null,
|
|
1892
|
-
keyBindingsModal: null,
|
|
1893
|
-
disconnectModal: null,
|
|
1894
|
-
reconnectModal: null,
|
|
1895
|
-
inDetailView: false,
|
|
1896
|
-
keyBindingView: false,
|
|
1897
|
-
inDisconnectView: false,
|
|
1898
|
-
inReconnectView: false,
|
|
1899
|
-
loadingBox: null,
|
|
1900
|
-
loadingView: false,
|
|
1901
|
-
fetchAbortController: null
|
|
1902
|
-
};
|
|
1903
|
-
this.exitPromiseResolve = null;
|
|
1904
|
-
this.urls = props.urls;
|
|
1905
|
-
this.greet = props.greet || "";
|
|
1906
|
-
this.tunnelConfig = props.tunnelConfig;
|
|
1907
|
-
this.disconnectInfo = props.disconnectInfo;
|
|
1908
|
-
if (props.tunnelInstance) {
|
|
1909
|
-
this.tunnelInstance = props.tunnelInstance;
|
|
1910
|
-
}
|
|
1911
|
-
this.exitPromise = new Promise((resolve) => {
|
|
1912
|
-
this.exitPromiseResolve = resolve;
|
|
1913
|
-
});
|
|
1914
|
-
this.screen = blessed3.screen({
|
|
1915
|
-
smartCSR: true,
|
|
1916
|
-
title: "Pinggy Tunnel",
|
|
1917
|
-
fullUnicode: true
|
|
1918
|
-
});
|
|
1919
|
-
this.setupStatsListener();
|
|
1920
|
-
this.setupWebDebugger();
|
|
1921
|
-
this.generateQrCodes();
|
|
1922
|
-
this.createUI();
|
|
1923
|
-
this.setupKeyBindings();
|
|
1924
|
-
}
|
|
1925
|
-
setupStatsListener() {
|
|
1926
|
-
globalThis.__PINGGY_TUNNEL_STATS__ = (newStats) => {
|
|
1927
|
-
this.stats = { ...newStats };
|
|
1928
|
-
this.updateStatsDisplay();
|
|
1929
|
-
};
|
|
1930
|
-
}
|
|
1931
|
-
clearSelection() {
|
|
1932
|
-
this.selectedIndex = -1;
|
|
1933
|
-
this.selectedRequestKey = null;
|
|
1934
|
-
}
|
|
1935
|
-
setupWebDebugger() {
|
|
1936
|
-
if (this.tunnelConfig?.webDebugger) {
|
|
1937
|
-
this.webDebuggerConnection = createWebDebuggerConnection(
|
|
1938
|
-
this.tunnelConfig.webDebugger,
|
|
1939
|
-
(pairs) => {
|
|
1940
|
-
this.pairs = pairs;
|
|
1941
|
-
if (this.selectedRequestKey !== null) {
|
|
1942
|
-
const newIndex = pairs.findIndex(
|
|
1943
|
-
(pair) => pair.request?.key === this.selectedRequestKey
|
|
1944
|
-
);
|
|
1945
|
-
if (newIndex !== -1) {
|
|
1946
|
-
this.selectedIndex = newIndex;
|
|
1947
|
-
} else {
|
|
1948
|
-
this.clearSelection();
|
|
1949
|
-
}
|
|
1950
|
-
}
|
|
1951
|
-
this.updateRequestsDisplay();
|
|
1952
|
-
}
|
|
1953
|
-
);
|
|
1954
|
-
}
|
|
1955
|
-
}
|
|
1956
|
-
async generateQrCodes() {
|
|
1957
|
-
if (this.tunnelConfig?.isQRCode && this.urls.length > 0) {
|
|
1958
|
-
this.qrCodes = await createQrCodes(this.urls);
|
|
1959
|
-
this.updateQrCodeDisplay();
|
|
1960
|
-
}
|
|
1961
|
-
}
|
|
1962
|
-
// Create the UI based on terminal size
|
|
1963
|
-
createUI() {
|
|
1964
|
-
this.buildUI();
|
|
1965
|
-
this.screen.on("resize", () => {
|
|
1966
|
-
this.handleResize();
|
|
1967
|
-
});
|
|
1968
|
-
}
|
|
1969
|
-
buildUI() {
|
|
1970
|
-
const width = this.screen.width;
|
|
1971
|
-
if (width < MIN_WIDTH_WARNING) {
|
|
1972
|
-
this.uiElements = {
|
|
1973
|
-
mainContainer: createWarningUI(this.screen),
|
|
1974
|
-
urlsBox: null,
|
|
1975
|
-
statsBox: null,
|
|
1976
|
-
requestsBox: null,
|
|
1977
|
-
footerBox: null,
|
|
1978
|
-
warningBox: createWarningUI(this.screen)
|
|
1979
|
-
};
|
|
1980
|
-
this.screen.render();
|
|
1981
|
-
return;
|
|
1982
|
-
}
|
|
1983
|
-
if (width < SIMPLE_LAYOUT_THRESHOLD) {
|
|
1984
|
-
this.uiElements = createSimpleUI(this.screen, this.urls, this.greet);
|
|
1985
|
-
} else {
|
|
1986
|
-
this.uiElements = createFullUI(this.screen, this.urls, this.greet, this.tunnelConfig);
|
|
1987
|
-
}
|
|
1988
|
-
this.refreshDisplays();
|
|
1989
|
-
this.screen.render();
|
|
1990
|
-
}
|
|
1991
|
-
refreshDisplays() {
|
|
1992
|
-
this.updateUrlsDisplay();
|
|
1993
|
-
this.updateStatsDisplay();
|
|
1994
|
-
this.updateRequestsDisplay();
|
|
1995
|
-
this.updateQrCodeDisplay();
|
|
1996
|
-
}
|
|
1997
|
-
updateUrlsDisplay() {
|
|
1998
|
-
updateUrlsDisplay(
|
|
1999
|
-
this.uiElements?.urlsBox,
|
|
2000
|
-
this.screen,
|
|
2001
|
-
this.urls,
|
|
2002
|
-
this.currentQrIndex
|
|
2003
|
-
);
|
|
2004
|
-
}
|
|
2005
|
-
updateStatsDisplay() {
|
|
2006
|
-
updateStatsDisplay(
|
|
2007
|
-
this.uiElements?.statsBox,
|
|
2008
|
-
this.screen,
|
|
2009
|
-
this.stats
|
|
2010
|
-
);
|
|
2011
|
-
}
|
|
2012
|
-
updateRequestsDisplay() {
|
|
2013
|
-
const result = updateRequestsDisplay(
|
|
2014
|
-
this.uiElements?.requestsBox,
|
|
2015
|
-
this.screen,
|
|
2016
|
-
this.pairs,
|
|
2017
|
-
this.selectedIndex
|
|
2018
|
-
);
|
|
2019
|
-
if (result.adjustedSelectedIndex !== this.selectedIndex) {
|
|
2020
|
-
if (result.adjustedSelectedIndex === -1) {
|
|
2021
|
-
this.clearSelection();
|
|
2022
|
-
} else {
|
|
2023
|
-
this.selectedIndex = result.adjustedSelectedIndex;
|
|
2024
|
-
}
|
|
2025
|
-
}
|
|
2026
|
-
if (result.trimmedPairs !== this.pairs) {
|
|
2027
|
-
this.pairs = result.trimmedPairs;
|
|
2028
|
-
}
|
|
2029
|
-
}
|
|
2030
|
-
updateQrCodeDisplay() {
|
|
2031
|
-
updateQrCodeDisplay(
|
|
2032
|
-
this.uiElements?.qrCodeBox,
|
|
2033
|
-
this.screen,
|
|
2034
|
-
this.qrCodes,
|
|
2035
|
-
this.urls,
|
|
2036
|
-
this.currentQrIndex
|
|
2037
|
-
);
|
|
2038
|
-
}
|
|
2039
|
-
setupKeyBindings() {
|
|
2040
|
-
const self = this;
|
|
2041
|
-
const state = {
|
|
2042
|
-
get currentQrIndex() {
|
|
2043
|
-
return self.currentQrIndex;
|
|
2044
|
-
},
|
|
2045
|
-
set currentQrIndex(value) {
|
|
2046
|
-
self.currentQrIndex = value;
|
|
2047
|
-
},
|
|
2048
|
-
get selectedIndex() {
|
|
2049
|
-
return self.selectedIndex;
|
|
2050
|
-
},
|
|
2051
|
-
set selectedIndex(value) {
|
|
2052
|
-
self.selectedIndex = value;
|
|
2053
|
-
},
|
|
2054
|
-
get pairs() {
|
|
2055
|
-
return self.pairs;
|
|
2056
|
-
},
|
|
2057
|
-
get urls() {
|
|
2058
|
-
return self.urls;
|
|
2059
|
-
}
|
|
2060
|
-
};
|
|
2061
|
-
const callbacks = {
|
|
2062
|
-
onQrIndexChange: (index) => {
|
|
2063
|
-
self.currentQrIndex = index;
|
|
2064
|
-
},
|
|
2065
|
-
onSelectedIndexChange: (index, requestKey) => {
|
|
2066
|
-
self.selectedIndex = index;
|
|
2067
|
-
self.selectedRequestKey = requestKey;
|
|
2068
|
-
},
|
|
2069
|
-
onDestroy: () => self.destroy(),
|
|
2070
|
-
updateUrlsDisplay: () => self.updateUrlsDisplay(),
|
|
2071
|
-
updateQrCodeDisplay: () => self.updateQrCodeDisplay(),
|
|
2072
|
-
updateRequestsDisplay: () => self.updateRequestsDisplay()
|
|
2073
|
-
};
|
|
2074
|
-
setupKeyBindings(
|
|
2075
|
-
this.screen,
|
|
2076
|
-
this.modalManager,
|
|
2077
|
-
state,
|
|
2078
|
-
callbacks,
|
|
2079
|
-
this.tunnelConfig
|
|
2080
|
-
);
|
|
2081
|
-
}
|
|
2082
|
-
handleResize() {
|
|
2083
|
-
this.screen.children.forEach((child) => child.destroy());
|
|
2084
|
-
this.buildUI();
|
|
2085
|
-
}
|
|
2086
|
-
updateDisconnectInfo(info) {
|
|
2087
|
-
this.disconnectInfo = info;
|
|
2088
|
-
if (info?.disconnected) {
|
|
2089
|
-
const message = info.error ? `Error: ${info.error}
|
|
2090
|
-
Tunnel will be closed.` : info.messages?.join("\n") || "Disconnect request received. Tunnel will be closed.";
|
|
2091
|
-
showDisconnectModal(
|
|
2092
|
-
this.screen,
|
|
2093
|
-
this.modalManager,
|
|
2094
|
-
message,
|
|
2095
|
-
() => this.destroy()
|
|
2096
|
-
);
|
|
2097
|
-
}
|
|
2098
|
-
}
|
|
2099
|
-
updateReconnectingInfo(retryCnt, message) {
|
|
2100
|
-
showReconnectingModal(
|
|
2101
|
-
this.screen,
|
|
2102
|
-
this.modalManager,
|
|
2103
|
-
retryCnt,
|
|
2104
|
-
message
|
|
2105
|
-
);
|
|
2106
|
-
}
|
|
2107
|
-
closeReconnectingInfo() {
|
|
2108
|
-
closeReconnectingModal(this.screen, this.modalManager);
|
|
2109
|
-
}
|
|
2110
|
-
updateReconnectionFailed(retryCnt) {
|
|
2111
|
-
showReconnectionFailedModal(
|
|
2112
|
-
this.screen,
|
|
2113
|
-
this.modalManager,
|
|
2114
|
-
retryCnt,
|
|
2115
|
-
() => this.destroy()
|
|
2116
|
-
);
|
|
2117
|
-
}
|
|
2118
|
-
start() {
|
|
2119
|
-
this.screen.render();
|
|
2120
|
-
}
|
|
2121
|
-
waitUntilExit() {
|
|
2122
|
-
return this.exitPromise;
|
|
2123
|
-
}
|
|
2124
|
-
destroy() {
|
|
2125
|
-
if (this.tunnelInstance?.tunnelid) {
|
|
2126
|
-
const manager = TunnelManager.getInstance();
|
|
2127
|
-
manager.stopTunnel(this.tunnelInstance.tunnelid);
|
|
2128
|
-
}
|
|
2129
|
-
delete globalThis.__PINGGY_TUNNEL_STATS__;
|
|
2130
|
-
if (this.webDebuggerConnection) {
|
|
2131
|
-
this.webDebuggerConnection.close();
|
|
2132
|
-
}
|
|
2133
|
-
this.screen.destroy();
|
|
2134
|
-
if (this.exitPromiseResolve) {
|
|
2135
|
-
this.exitPromiseResolve();
|
|
2136
|
-
}
|
|
2137
|
-
}
|
|
2138
|
-
};
|
|
2139
|
-
|
|
2140
|
-
// src/cli/starCli.ts
|
|
2141
|
-
var TunnelData = {
|
|
2142
|
-
urls: null,
|
|
2143
|
-
greet: null,
|
|
2144
|
-
usage: null
|
|
2145
|
-
};
|
|
2146
|
-
var activeTui = null;
|
|
2147
|
-
var disconnectState = null;
|
|
2148
|
-
async function launchTui(finalConfig, urls, greet, tunnel) {
|
|
2149
|
-
try {
|
|
2150
|
-
const isTTYEnabled = process.stdin.isTTY;
|
|
2151
|
-
if (!isTTYEnabled) {
|
|
2152
|
-
printer_default.warn("Unable to initiate the TUI: your terminal does not support the required input mode.");
|
|
2153
|
-
return;
|
|
2154
|
-
}
|
|
2155
|
-
const tui = new TunnelTui({
|
|
2156
|
-
urls: urls ?? [],
|
|
2157
|
-
greet: greet ?? "",
|
|
2158
|
-
tunnelConfig: finalConfig,
|
|
2159
|
-
disconnectInfo: null,
|
|
2160
|
-
tunnelInstance: tunnel
|
|
2161
|
-
});
|
|
2162
|
-
activeTui = tui;
|
|
2163
|
-
try {
|
|
2164
|
-
tui.start();
|
|
2165
|
-
await tui.waitUntilExit();
|
|
2166
|
-
} catch (e) {
|
|
2167
|
-
logger.warn("TUI error", e);
|
|
2168
|
-
} finally {
|
|
2169
|
-
activeTui = null;
|
|
2170
|
-
}
|
|
2171
|
-
} catch (e) {
|
|
2172
|
-
logger.warn("Failed to (re-)initiate TUI", e);
|
|
2173
|
-
}
|
|
2174
|
-
}
|
|
2175
|
-
async function startCli(finalConfig, manager) {
|
|
2176
|
-
if (!finalConfig.optional?.noTui && finalConfig.webDebugger === "") {
|
|
2177
|
-
const freePort = await getFreePort(finalConfig.webDebugger || "");
|
|
2178
|
-
finalConfig.webDebugger = `localhost:${freePort}`;
|
|
2179
|
-
}
|
|
2180
|
-
try {
|
|
2181
|
-
const manager2 = TunnelManager.getInstance();
|
|
2182
|
-
const tunnel = await manager2.createTunnel(finalConfig);
|
|
2183
|
-
printer_default.startSpinner("Connecting to Pinggy...");
|
|
2184
|
-
if (!finalConfig.optional?.noTui) {
|
|
2185
|
-
manager2.registerStatsListener(tunnel.tunnelid, (tunnelId, stats) => {
|
|
2186
|
-
globalThis.__PINGGY_TUNNEL_STATS__?.(stats);
|
|
2187
|
-
});
|
|
2188
|
-
}
|
|
2189
|
-
manager2.registerWorkerErrorListner(tunnel.tunnelid, (_tunnelid, error) => {
|
|
2190
|
-
printer_default.fatal(`${error.message}`);
|
|
2191
|
-
});
|
|
2192
|
-
await manager2.startTunnel(tunnel.tunnelid);
|
|
2193
|
-
printer_default.stopSpinnerSuccess(" Connected to Pinggy");
|
|
2194
|
-
printer_default.success(pico.bold("Tunnel established!"));
|
|
2195
|
-
printer_default.print(pico.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"));
|
|
2196
|
-
TunnelData.urls = await manager2.getTunnelUrls(tunnel.tunnelid);
|
|
2197
|
-
TunnelData.greet = await manager2.getTunnelGreetMessage(tunnel.tunnelid);
|
|
2198
|
-
printer_default.info(pico.cyanBright("Remote URLs:"));
|
|
2199
|
-
(TunnelData.urls ?? []).forEach(
|
|
2200
|
-
(url) => printer_default.print(" " + pico.magentaBright(url))
|
|
2201
|
-
);
|
|
2202
|
-
printer_default.print(pico.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"));
|
|
2203
|
-
if (TunnelData.greet?.includes("not authenticated")) {
|
|
2204
|
-
printer_default.warn(pico.yellowBright(TunnelData.greet));
|
|
2205
|
-
} else if (TunnelData.greet?.includes("authenticated as")) {
|
|
2206
|
-
const emailMatch = /authenticated as (.+)/.exec(TunnelData.greet);
|
|
2207
|
-
if (emailMatch) {
|
|
2208
|
-
const email = emailMatch[1];
|
|
2209
|
-
printer_default.info(pico.cyanBright("Authenticated as: " + email));
|
|
2210
|
-
}
|
|
2211
|
-
}
|
|
2212
|
-
printer_default.print(pico.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"));
|
|
2213
|
-
printer_default.print(pico.gray("\nPress Ctrl+C to stop the tunnel.\n"));
|
|
2214
|
-
manager2.registerWillReconnectListener(tunnel.tunnelid, (tunnelId, error, messages) => {
|
|
2215
|
-
if (activeTui) {
|
|
2216
|
-
const msg = messages?.join("\n") || error || "Tunnel disconnected, reconnecting...";
|
|
2217
|
-
activeTui.updateReconnectingInfo(0, msg);
|
|
2218
|
-
} else if (finalConfig.autoReconnect) {
|
|
2219
|
-
printer_default.warn(error || "Tunnel connection reset");
|
|
2220
|
-
printer_default.startSpinner(messages?.join("\n"));
|
|
2221
|
-
}
|
|
2222
|
-
});
|
|
2223
|
-
manager2.registerReconnectingListener(tunnel.tunnelid, (tunnelId, retryCnt) => {
|
|
2224
|
-
if (activeTui) {
|
|
2225
|
-
activeTui.updateReconnectingInfo(retryCnt);
|
|
2226
|
-
} else if (finalConfig.autoReconnect) {
|
|
2227
|
-
printer_default.startSpinner(`Reconnecting to Pinggy (attempt #${retryCnt})`);
|
|
2228
|
-
}
|
|
2229
|
-
});
|
|
2230
|
-
manager2.registerReconnectionCompletedListener(tunnel.tunnelid, (tunnelId, urls) => {
|
|
2231
|
-
if (activeTui) {
|
|
2232
|
-
activeTui.closeReconnectingInfo();
|
|
2233
|
-
}
|
|
2234
|
-
});
|
|
2235
|
-
manager2.registerReconnectionFailedListener(tunnel.tunnelid, (tunnelId, retryCnt) => {
|
|
2236
|
-
if (activeTui) {
|
|
2237
|
-
activeTui.updateReconnectionFailed(retryCnt);
|
|
2238
|
-
} else {
|
|
2239
|
-
printer_default.stopSpinnerFail(`Reconnection failed after ${retryCnt} attempts`);
|
|
2240
|
-
process.exit(1);
|
|
2241
|
-
}
|
|
2242
|
-
});
|
|
2243
|
-
manager2.registerDisconnectListener(tunnel.tunnelid, async (tunnelId, error, messages) => {
|
|
2244
|
-
if (activeTui) {
|
|
2245
|
-
disconnectState = {
|
|
2246
|
-
disconnected: true,
|
|
2247
|
-
error,
|
|
2248
|
-
messages
|
|
2249
|
-
};
|
|
2250
|
-
activeTui.updateDisconnectInfo(disconnectState);
|
|
2251
|
-
try {
|
|
2252
|
-
await activeTui.waitUntilExit();
|
|
2253
|
-
} catch (e) {
|
|
2254
|
-
logger.warn("Failed to wait for TUI exit", e);
|
|
2255
|
-
} finally {
|
|
2256
|
-
activeTui = null;
|
|
2257
|
-
printer_default.warn(`Error in tunnel:`);
|
|
2258
|
-
messages?.forEach(function(m) {
|
|
2259
|
-
printer_default.warnTxt(m);
|
|
2260
|
-
});
|
|
2261
|
-
if (!finalConfig.autoReconnect) {
|
|
2262
|
-
process.exit(0);
|
|
2263
|
-
}
|
|
2264
|
-
}
|
|
2265
|
-
} else {
|
|
2266
|
-
messages?.forEach(function(m) {
|
|
2267
|
-
printer_default.warn(m);
|
|
2268
|
-
});
|
|
2269
|
-
if (!finalConfig.autoReconnect) {
|
|
2270
|
-
process.exit(0);
|
|
2271
|
-
}
|
|
2272
|
-
}
|
|
2273
|
-
if (finalConfig.autoReconnect) {
|
|
2274
|
-
printer_default.startSpinner("Reconnecting to Pinggy");
|
|
2275
|
-
}
|
|
2276
|
-
});
|
|
2277
|
-
try {
|
|
2278
|
-
await manager2.registerStartListener(tunnel.tunnelid, async (tunnelId, urls) => {
|
|
2279
|
-
try {
|
|
2280
|
-
printer_default.stopSpinnerSuccess("Reconnected to Pinggy");
|
|
2281
|
-
} catch (e) {
|
|
2282
|
-
}
|
|
2283
|
-
printer_default.success(pico.bold("Tunnel re-established!"));
|
|
2284
|
-
printer_default.print(pico.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"));
|
|
2285
|
-
TunnelData.urls = urls;
|
|
2286
|
-
TunnelData.greet = await manager2.getTunnelGreetMessage(tunnel.tunnelid);
|
|
2287
|
-
printer_default.info(pico.cyanBright("Remote URLs:"));
|
|
2288
|
-
(TunnelData.urls ?? []).forEach(
|
|
2289
|
-
(url) => printer_default.print(" " + pico.magentaBright(url))
|
|
2290
|
-
);
|
|
2291
|
-
printer_default.print(pico.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"));
|
|
2292
|
-
if (TunnelData.greet?.includes("not authenticated")) {
|
|
2293
|
-
printer_default.warn(pico.yellowBright(TunnelData.greet));
|
|
2294
|
-
} else if (TunnelData.greet?.includes("authenticated as")) {
|
|
2295
|
-
const emailMatch = /authenticated as (.+)/.exec(TunnelData.greet);
|
|
2296
|
-
if (emailMatch) {
|
|
2297
|
-
const email = emailMatch[1];
|
|
2298
|
-
printer_default.info(pico.cyanBright("Authenticated as: " + email));
|
|
2299
|
-
}
|
|
2300
|
-
}
|
|
2301
|
-
printer_default.print(pico.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"));
|
|
2302
|
-
printer_default.print(pico.gray("\nPress Ctrl+C to stop the tunnel.\n"));
|
|
2303
|
-
if (!finalConfig.optional?.noTui) {
|
|
2304
|
-
await launchTui(finalConfig, TunnelData.urls, TunnelData.greet, tunnel);
|
|
2305
|
-
}
|
|
2306
|
-
});
|
|
2307
|
-
} catch (e) {
|
|
2308
|
-
logger.debug("Failed to register start listener", e);
|
|
2309
|
-
}
|
|
2310
|
-
if (!finalConfig.optional?.noTui) {
|
|
2311
|
-
await launchTui(finalConfig, TunnelData.urls, TunnelData.greet, tunnel);
|
|
2312
|
-
}
|
|
2313
|
-
} catch (err) {
|
|
2314
|
-
printer_default.stopSpinnerFail("Failed to connect");
|
|
2315
|
-
printer_default.fatal(err.message || "Unknown error");
|
|
2316
|
-
throw err;
|
|
2317
|
-
}
|
|
2318
|
-
}
|
|
2319
|
-
|
|
2320
|
-
// src/cli/configStore.ts
|
|
2321
|
-
import fs3 from "fs";
|
|
2322
|
-
import path3 from "path";
|
|
2323
|
-
|
|
2324
|
-
// src/utils/configDir.ts
|
|
2325
|
-
import os2 from "os";
|
|
2326
|
-
import path2 from "path";
|
|
2327
|
-
import fs2 from "fs";
|
|
2328
|
-
function getPinggyConfigDir() {
|
|
2329
|
-
const platform2 = os2.platform();
|
|
2330
|
-
let baseDir;
|
|
2331
|
-
if (platform2 === "win32") {
|
|
2332
|
-
baseDir = process.env.APPDATA || path2.join(os2.homedir(), "AppData", "Roaming");
|
|
2333
|
-
} else {
|
|
2334
|
-
baseDir = process.env.XDG_CONFIG_HOME || path2.join(os2.homedir(), ".config");
|
|
2335
|
-
}
|
|
2336
|
-
return path2.join(baseDir, "pinggy");
|
|
2337
|
-
}
|
|
2338
|
-
function getTunnelConfigDir() {
|
|
2339
|
-
return path2.join(getPinggyConfigDir(), "tunnels");
|
|
2340
|
-
}
|
|
2341
|
-
function ensureTunnelConfigDir() {
|
|
2342
|
-
const dir = getTunnelConfigDir();
|
|
2343
|
-
fs2.mkdirSync(dir, { recursive: true });
|
|
2344
|
-
return dir;
|
|
2345
|
-
}
|
|
2346
|
-
|
|
2347
|
-
// src/cli/configStore.ts
|
|
2348
|
-
import pico2 from "picocolors";
|
|
2349
|
-
function buildFilename(name, configId) {
|
|
2350
|
-
return `${name}_${configId}.json`;
|
|
2351
|
-
}
|
|
2352
|
-
function sanitizeName(name) {
|
|
2353
|
-
return name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
2354
|
-
}
|
|
2355
|
-
function validateName(name) {
|
|
2356
|
-
if (!name || name.trim().length === 0) {
|
|
2357
|
-
return new Error("Tunnel name cannot be empty.");
|
|
2358
|
-
}
|
|
2359
|
-
if (name.length > 128) {
|
|
2360
|
-
return new Error("Tunnel name cannot exceed 128 characters.");
|
|
2361
|
-
}
|
|
2362
|
-
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
2363
|
-
return new Error("Tunnel name can only contain alphanumeric characters, hyphens, and underscores.");
|
|
2364
|
-
}
|
|
2365
|
-
return null;
|
|
2366
|
-
}
|
|
2367
|
-
function readConfigFile(filePath) {
|
|
2368
|
-
try {
|
|
2369
|
-
const data = fs3.readFileSync(filePath, { encoding: "utf-8" });
|
|
2370
|
-
return JSON.parse(data);
|
|
2371
|
-
} catch (err) {
|
|
2372
|
-
logger.warn(`Failed to read config file ${filePath}:`, err);
|
|
2373
|
-
return null;
|
|
2374
|
-
}
|
|
2375
|
-
}
|
|
2376
|
-
function writeConfigFile(filePath, config) {
|
|
2377
|
-
fs3.writeFileSync(filePath, JSON.stringify(config, null, 2), { encoding: "utf-8" });
|
|
2378
|
-
}
|
|
2379
|
-
function listSavedConfigs() {
|
|
2380
|
-
const dir = getTunnelConfigDir();
|
|
2381
|
-
if (!fs3.existsSync(dir)) {
|
|
2382
|
-
return [];
|
|
2383
|
-
}
|
|
2384
|
-
const files = fs3.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
2385
|
-
const configs = [];
|
|
2386
|
-
for (const file of files) {
|
|
2387
|
-
const config = readConfigFile(path3.join(dir, file));
|
|
2388
|
-
if (config && config.name && config.configId) {
|
|
2389
|
-
configs.push(config);
|
|
2390
|
-
}
|
|
2391
|
-
}
|
|
2392
|
-
return configs;
|
|
2393
|
-
}
|
|
2394
|
-
function findConfigFile(nameOrId) {
|
|
2395
|
-
const dir = getTunnelConfigDir();
|
|
2396
|
-
if (!fs3.existsSync(dir)) return null;
|
|
2397
|
-
const files = fs3.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
2398
|
-
const sanitized = sanitizeName(nameOrId);
|
|
2399
|
-
const nameMatch = files.find((f) => f.startsWith(sanitized + "_"));
|
|
2400
|
-
if (nameMatch) {
|
|
2401
|
-
const filePath = path3.join(dir, nameMatch);
|
|
2402
|
-
const config = readConfigFile(filePath);
|
|
2403
|
-
if (config && config.name === nameOrId) return { filePath, config };
|
|
2404
|
-
}
|
|
2405
|
-
const idCandidates = files.filter((f) => {
|
|
2406
|
-
const withoutExt = f.replace(/\.json$/, "");
|
|
2407
|
-
const lastUnderscore = withoutExt.indexOf("_");
|
|
2408
|
-
if (lastUnderscore === -1) return false;
|
|
2409
|
-
const idPart = withoutExt.slice(lastUnderscore + 1);
|
|
2410
|
-
return idPart.startsWith(nameOrId);
|
|
2411
|
-
});
|
|
2412
|
-
if (idCandidates.length === 1) {
|
|
2413
|
-
const filePath = path3.join(dir, idCandidates[0]);
|
|
2414
|
-
const config = readConfigFile(filePath);
|
|
2415
|
-
if (config) return { filePath, config };
|
|
2416
|
-
}
|
|
2417
|
-
return null;
|
|
2418
|
-
}
|
|
2419
|
-
function findConfigByName(name) {
|
|
2420
|
-
const resolved = findConfigFile(name);
|
|
2421
|
-
return resolved?.config.name === name ? resolved.config : null;
|
|
2422
|
-
}
|
|
2423
|
-
function findConfig(nameOrId) {
|
|
2424
|
-
return findConfigFile(nameOrId)?.config ?? null;
|
|
2425
|
-
}
|
|
2426
|
-
function saveConfig(name, configId, tunnelConfig, autoStart = false) {
|
|
2427
|
-
const nameErr = validateName(name);
|
|
2428
|
-
if (nameErr) {
|
|
2429
|
-
throw nameErr;
|
|
2430
|
-
}
|
|
2431
|
-
const existing = findConfigByName(name);
|
|
2432
|
-
if (existing) {
|
|
2433
|
-
throw new Error(
|
|
2434
|
-
`A tunnel config with the name "${name}" already exists (configId: ${existing.configId}). Please use a different name.`
|
|
2435
|
-
);
|
|
2436
|
-
}
|
|
2437
|
-
const dir = ensureTunnelConfigDir();
|
|
2438
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2439
|
-
const saved = {
|
|
2440
|
-
name,
|
|
2441
|
-
configId,
|
|
2442
|
-
autoStart,
|
|
2443
|
-
createdAt: now,
|
|
2444
|
-
updatedAt: now,
|
|
2445
|
-
tunnelConfig
|
|
2446
|
-
};
|
|
2447
|
-
const filename = buildFilename(sanitizeName(name), configId);
|
|
2448
|
-
const filePath = path3.join(dir, filename);
|
|
2449
|
-
fs3.writeFileSync(filePath, JSON.stringify(saved, null, 2), { encoding: "utf-8" });
|
|
2450
|
-
logger.info(`Config "${name}" saved to ${filePath}`);
|
|
2451
|
-
return saved;
|
|
2452
|
-
}
|
|
2453
|
-
function deleteConfig(nameOrId) {
|
|
2454
|
-
const resolved = findConfigFile(nameOrId);
|
|
2455
|
-
if (!resolved) return null;
|
|
2456
|
-
fs3.unlinkSync(resolved.filePath);
|
|
2457
|
-
logger.info(`Config "${resolved.config.name}" deleted.`);
|
|
2458
|
-
return resolved.config.name;
|
|
2459
|
-
}
|
|
2460
|
-
function updateConfigAutoStart(nameOrId, autoStart) {
|
|
2461
|
-
const resolved = findConfigFile(nameOrId);
|
|
2462
|
-
if (!resolved) return null;
|
|
2463
|
-
resolved.config.autoStart = autoStart;
|
|
2464
|
-
resolved.config.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2465
|
-
writeConfigFile(resolved.filePath, resolved.config);
|
|
2466
|
-
logger.info(`Config "${resolved.config.name}" auto-start set to ${autoStart}`);
|
|
2467
|
-
return resolved.config;
|
|
2468
|
-
}
|
|
2469
|
-
function updateTunnelConfig(nameOrId, tunnelConfig) {
|
|
2470
|
-
const resolved = findConfigFile(nameOrId);
|
|
2471
|
-
if (!resolved) return null;
|
|
2472
|
-
resolved.config.tunnelConfig = tunnelConfig;
|
|
2473
|
-
resolved.config.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2474
|
-
writeConfigFile(resolved.filePath, resolved.config);
|
|
2475
|
-
logger.info(`Config "${resolved.config.name}" tunnel configuration updated`);
|
|
2476
|
-
return resolved.config;
|
|
2477
|
-
}
|
|
2478
|
-
function getAutoStartConfigs() {
|
|
2479
|
-
return listSavedConfigs().filter((c) => c.autoStart);
|
|
2480
|
-
}
|
|
2481
|
-
function printConfigList() {
|
|
2482
|
-
const configs = listSavedConfigs();
|
|
2483
|
-
if (configs.length === 0) {
|
|
2484
|
-
console.log(pico2.yellow("No saved tunnel configs found."));
|
|
2485
|
-
console.log(pico2.gray(`Config directory: ${getTunnelConfigDir()}`));
|
|
2486
|
-
return;
|
|
2487
|
-
}
|
|
2488
|
-
const nameW = 20;
|
|
2489
|
-
const idW = 12;
|
|
2490
|
-
const typeW = 8;
|
|
2491
|
-
const fwdW = 25;
|
|
2492
|
-
const serverW = 22;
|
|
2493
|
-
const autoW = 10;
|
|
2494
|
-
const header = pico2.bold("Name".padEnd(nameW)) + pico2.bold("Config ID".padEnd(idW)) + pico2.bold("Type".padEnd(typeW)) + pico2.bold("Forwarding".padEnd(fwdW)) + pico2.bold("Server".padEnd(serverW)) + pico2.bold("Auto-start".padEnd(autoW));
|
|
2495
|
-
console.log("\n" + header);
|
|
2496
|
-
console.log(pico2.gray("\u2500".repeat(nameW + idW + typeW + fwdW + serverW + autoW)));
|
|
2497
|
-
for (const c of configs) {
|
|
2498
|
-
const tc = c.tunnelConfig;
|
|
2499
|
-
const forwarding = Array.isArray(tc.forwarding) ? tc.forwarding[0]?.address : String(tc.forwarding || "");
|
|
2500
|
-
const type = (Array.isArray(tc.forwarding) ? tc.forwarding[0]?.type : void 0) || "http";
|
|
2501
|
-
const server = tc.serverAddress || "a.pinggy.io";
|
|
2502
|
-
const line = pico2.cyanBright(c.name.padEnd(nameW)) + pico2.gray(c.configId.slice(0, 8).padEnd(idW)) + type.padEnd(typeW) + forwarding.slice(0, fwdW - 2).padEnd(fwdW) + server.slice(0, serverW - 2).padEnd(serverW) + (c.autoStart ? pico2.green("yes") : pico2.gray("no")).padEnd(autoW);
|
|
2503
|
-
console.log(line);
|
|
2504
|
-
}
|
|
2505
|
-
console.log();
|
|
2506
|
-
}
|
|
2507
|
-
function printConfigDetail(config) {
|
|
2508
|
-
console.log(pico2.bold(`
|
|
2509
|
-
Tunnel Config: ${pico2.cyanBright(config.name)}`));
|
|
2510
|
-
console.log(pico2.gray("\u2500".repeat(40)));
|
|
2511
|
-
console.log(` Config ID: ${config.configId}`);
|
|
2512
|
-
console.log(` Auto-start: ${config.autoStart ? pico2.green("yes") : pico2.gray("no")}`);
|
|
2513
|
-
console.log(` Created: ${config.createdAt}`);
|
|
2514
|
-
console.log(` Updated: ${config.updatedAt}`);
|
|
2515
|
-
console.log(pico2.gray("\u2500".repeat(40)));
|
|
2516
|
-
console.log(` Server: ${config.tunnelConfig.serverAddress || "a.pinggy.io"}`);
|
|
2517
|
-
console.log(` Token: ${config.tunnelConfig.token ? "***" + config.tunnelConfig.token.slice(-4) : "(none)"}`);
|
|
2518
|
-
const fwd = config.tunnelConfig.forwarding;
|
|
2519
|
-
if (Array.isArray(fwd)) {
|
|
2520
|
-
const defaultFwds = [];
|
|
2521
|
-
const customFwds = [];
|
|
2522
|
-
for (const f of fwd) {
|
|
2523
|
-
if (typeof f === "string") {
|
|
2524
|
-
defaultFwds.push(f);
|
|
2525
|
-
} else if (f.listenAddress) {
|
|
2526
|
-
customFwds.push(f);
|
|
2527
|
-
} else {
|
|
2528
|
-
defaultFwds.push(f);
|
|
2529
|
-
}
|
|
2530
|
-
}
|
|
2531
|
-
for (const f of defaultFwds) {
|
|
2532
|
-
const addr = typeof f === "string" ? f : `${f.address} (${f.type || "http"})`;
|
|
2533
|
-
console.log(` Forwarding: ${addr}`);
|
|
2534
|
-
if (config.tunnelConfig.webDebugger) {
|
|
2535
|
-
console.log(` Debugger: ${config.tunnelConfig.webDebugger}`);
|
|
2536
|
-
}
|
|
2537
|
-
}
|
|
2538
|
-
if (customFwds.length > 0) {
|
|
2539
|
-
console.log(pico2.gray("\u2500".repeat(40)));
|
|
2540
|
-
console.log(pico2.bold(" Domain Mappings:"));
|
|
2541
|
-
for (const f of customFwds) {
|
|
2542
|
-
if (typeof f === "string") continue;
|
|
2543
|
-
const domain = f.listenAddress;
|
|
2544
|
-
const target = f.address;
|
|
2545
|
-
const type = f.type || "http";
|
|
2546
|
-
console.log(` ${pico2.cyanBright(domain)} \u2192 ${target} (${type})`);
|
|
2547
|
-
}
|
|
2548
|
-
}
|
|
2549
|
-
} else if (fwd) {
|
|
2550
|
-
console.log(` Forwarding: ${fwd}`);
|
|
2551
|
-
}
|
|
2552
|
-
console.log();
|
|
2553
|
-
}
|
|
2554
|
-
|
|
2555
|
-
// src/cli/buildAndStartTunnel.ts
|
|
2556
|
-
async function buildAndStartTunnel(values, positionals, manager) {
|
|
2557
|
-
await initRemoteManagement(values);
|
|
2558
|
-
logger.debug("Building final config from CLI values and positionals", { values, positionals });
|
|
2559
|
-
const finalConfig = await buildFinalConfig(values, positionals);
|
|
2560
|
-
logger.debug("Final configuration built", finalConfig);
|
|
2561
|
-
if (values.save) {
|
|
2562
|
-
const name = values.name;
|
|
2563
|
-
if (!name) {
|
|
2564
|
-
printer_default.error("--save requires --name to specify a name for the tunnel config.");
|
|
2565
|
-
process.exit(1);
|
|
2566
|
-
}
|
|
2567
|
-
const nameErr = validateName(name);
|
|
2568
|
-
if (nameErr) {
|
|
2569
|
-
printer_default.error(nameErr.message);
|
|
2570
|
-
process.exit(1);
|
|
2571
|
-
}
|
|
2572
|
-
const autoStart = !!values.auto;
|
|
2573
|
-
saveConfig(name, finalConfig.configId, finalConfig, autoStart);
|
|
2574
|
-
printer_default.success(`Config "${name}" saved.`);
|
|
2575
|
-
}
|
|
2576
|
-
await startCli(finalConfig, manager);
|
|
2577
|
-
}
|
|
2578
|
-
async function initRemoteManagement(values) {
|
|
2579
|
-
const parseResult = await parseRemoteManagement(values);
|
|
2580
|
-
if (parseResult?.ok === false) {
|
|
2581
|
-
logger.error("Failed to initiate remote management:", parseResult.error);
|
|
2582
|
-
printer_default.fatal(parseResult.error);
|
|
2583
|
-
}
|
|
2584
|
-
}
|
|
2585
|
-
|
|
2586
|
-
// src/cli/subcommands.ts
|
|
2587
|
-
import pico3 from "picocolors";
|
|
2588
|
-
var SUBCOMMANDS = /* @__PURE__ */ new Set(["config", "start"]);
|
|
2589
|
-
function isSubcommand(rawArgs) {
|
|
2590
|
-
return rawArgs.length > 0 && SUBCOMMANDS.has(rawArgs[0]);
|
|
2591
|
-
}
|
|
2592
|
-
async function handleSubcommand(rawArgs, manager) {
|
|
2593
|
-
const sub = rawArgs[0];
|
|
2594
|
-
const rest = rawArgs.slice(1);
|
|
2595
|
-
switch (sub) {
|
|
2596
|
-
case "config":
|
|
2597
|
-
await handleConfig(rest);
|
|
2598
|
-
return;
|
|
2599
|
-
case "start":
|
|
2600
|
-
await handleStart(rest, manager);
|
|
2601
|
-
return;
|
|
2602
|
-
}
|
|
2603
|
-
}
|
|
2604
|
-
async function handleConfig(args) {
|
|
2605
|
-
if (args.length === 0) {
|
|
2606
|
-
printConfigHelp();
|
|
2607
|
-
return;
|
|
2608
|
-
}
|
|
2609
|
-
const verb = args[0];
|
|
2610
|
-
const rest = args.slice(1);
|
|
2611
|
-
switch (verb) {
|
|
2612
|
-
case "list":
|
|
2613
|
-
case "ls":
|
|
2614
|
-
printConfigList();
|
|
2615
|
-
return;
|
|
2616
|
-
case "show": {
|
|
2617
|
-
const names = requireNames(rest, "config show");
|
|
2618
|
-
for (const name of names) {
|
|
2619
|
-
const saved2 = resolveConfig(name);
|
|
2620
|
-
if (saved2) printConfigDetail(saved2);
|
|
2621
|
-
}
|
|
2622
|
-
return;
|
|
2623
|
-
}
|
|
2624
|
-
case "save": {
|
|
2625
|
-
const name = requireName(rest, "config save");
|
|
2626
|
-
await handleConfigSave(name, rest.slice(1));
|
|
2627
|
-
return;
|
|
2628
|
-
}
|
|
2629
|
-
case "delete": {
|
|
2630
|
-
const names = requireNames(rest, "config delete");
|
|
2631
|
-
for (const name of names) {
|
|
2632
|
-
const deletedName = deleteConfig(name);
|
|
2633
|
-
if (deletedName) {
|
|
2634
|
-
printer_default.success(`Config "${deletedName}" deleted.`);
|
|
2635
|
-
} else {
|
|
2636
|
-
printer_default.error(`No config found matching "${name}". Use: pinggy config list`);
|
|
2637
|
-
}
|
|
2638
|
-
}
|
|
2639
|
-
return;
|
|
2640
|
-
}
|
|
2641
|
-
case "update": {
|
|
2642
|
-
const name = requireName(rest, "config update");
|
|
2643
|
-
await handleConfigUpdate(name, rest.slice(1));
|
|
2644
|
-
return;
|
|
2645
|
-
}
|
|
2646
|
-
case "auto": {
|
|
2647
|
-
const names = requireNames(rest, "config auto");
|
|
2648
|
-
for (const name of names) {
|
|
2649
|
-
const updated = updateConfigAutoStart(name, true);
|
|
2650
|
-
if (updated) {
|
|
2651
|
-
printer_default.success(`Config "${updated.name}" auto-start set to on.`);
|
|
2652
|
-
} else {
|
|
2653
|
-
printer_default.error(`No config found matching "${name}". Use: pinggy config list`);
|
|
2654
|
-
}
|
|
2655
|
-
}
|
|
2656
|
-
return;
|
|
2657
|
-
}
|
|
2658
|
-
case "noauto": {
|
|
2659
|
-
const names = requireNames(rest, "config noauto");
|
|
2660
|
-
for (const name of names) {
|
|
2661
|
-
const updated = updateConfigAutoStart(name, false);
|
|
2662
|
-
if (updated) {
|
|
2663
|
-
printer_default.success(`Config "${updated.name}" auto-start set to off.`);
|
|
2664
|
-
} else {
|
|
2665
|
-
printer_default.error(`No config found matching "${name}". Use: pinggy config list`);
|
|
2666
|
-
}
|
|
2667
|
-
}
|
|
2668
|
-
return;
|
|
2669
|
-
}
|
|
2670
|
-
default:
|
|
2671
|
-
const saved = resolveConfig(verb);
|
|
2672
|
-
if (saved) printConfigDetail(saved);
|
|
2673
|
-
return;
|
|
2674
|
-
}
|
|
2675
|
-
}
|
|
2676
|
-
async function handleConfigSave(name, remainingArgs) {
|
|
2677
|
-
const nameErr = validateName(name);
|
|
2678
|
-
if (nameErr) {
|
|
2679
|
-
printer_default.error(nameErr.message);
|
|
2680
|
-
process.exit(1);
|
|
2681
|
-
}
|
|
2682
|
-
const { values, positionals } = parseCliArgs(cliOptions, remainingArgs);
|
|
2683
|
-
const autoStart = !!values.auto;
|
|
2684
|
-
logger.debug("Building config for save", { name, values, positionals });
|
|
2685
|
-
const finalConfig = await buildFinalConfig(values, positionals);
|
|
2686
|
-
saveConfig(name, finalConfig.configId, finalConfig, autoStart);
|
|
2687
|
-
printer_default.success(`Config "${name}" saved.`);
|
|
2688
|
-
}
|
|
2689
|
-
async function handleConfigUpdate(nameOrId, remainingArgs) {
|
|
2690
|
-
const saved = resolveConfig(nameOrId);
|
|
2691
|
-
if (!saved) return;
|
|
2692
|
-
const { values, positionals } = parseCliArgs(cliOptions, remainingArgs);
|
|
2693
|
-
logger.debug("Building updated config", { nameOrId, values, positionals });
|
|
2694
|
-
const updatedConfig = await buildFinalConfig(values, positionals, saved.tunnelConfig);
|
|
2695
|
-
const result = updateTunnelConfig(nameOrId, updatedConfig);
|
|
2696
|
-
if (result) {
|
|
2697
|
-
printer_default.success(`Config "${result.name}" updated.`);
|
|
2698
|
-
printConfigDetail(result);
|
|
2699
|
-
} else {
|
|
2700
|
-
printer_default.error(`Failed to update config "${nameOrId}".`);
|
|
2701
|
-
}
|
|
2702
|
-
}
|
|
2703
|
-
async function handleStart(args, manager) {
|
|
2704
|
-
const startAll = args.includes("--all");
|
|
2705
|
-
const argsWithoutAll = args.filter((a) => a !== "--all");
|
|
2706
|
-
const names = [];
|
|
2707
|
-
let i = 0;
|
|
2708
|
-
while (i < argsWithoutAll.length && !argsWithoutAll[i].startsWith("-")) {
|
|
2709
|
-
names.push(argsWithoutAll[i]);
|
|
2710
|
-
i++;
|
|
2711
|
-
}
|
|
2712
|
-
const flagArgs = argsWithoutAll.slice(i);
|
|
2713
|
-
const { values, positionals } = parseCliArgs(cliOptions, flagArgs);
|
|
2714
|
-
configureLogger(values);
|
|
2715
|
-
if (startAll) {
|
|
2716
|
-
await initRemoteManagementBackground(values);
|
|
2717
|
-
await startAutoStartTunnels(manager);
|
|
2718
|
-
return;
|
|
2719
|
-
}
|
|
2720
|
-
if (names.length === 0) {
|
|
2721
|
-
printStartHelp();
|
|
2722
|
-
return;
|
|
2723
|
-
}
|
|
2724
|
-
const resolved = [];
|
|
2725
|
-
for (const name of names) {
|
|
2726
|
-
const saved = resolveConfig(name);
|
|
2727
|
-
if (!saved) return;
|
|
2728
|
-
resolved.push(saved);
|
|
2729
|
-
}
|
|
2730
|
-
if (resolved.length > 1 && flagArgs.length > 0) {
|
|
2731
|
-
printer_default.error("Runtime overrides (-l, --type, etc.) can only be used when starting a single tunnel.");
|
|
2732
|
-
printer_default.print(" Start one tunnel: pinggy start my-tunnel -l 4000");
|
|
2733
|
-
printer_default.print(" Or update first: pinggy config update my-tunnel -l 4000");
|
|
2734
|
-
return;
|
|
2735
|
-
}
|
|
2736
|
-
await initRemoteManagementBackground(values);
|
|
2737
|
-
if (resolved.length === 1) {
|
|
2738
|
-
const saved = resolved[0];
|
|
2739
|
-
logger.debug("Building config with overrides", { name: saved.name });
|
|
2740
|
-
const finalConfig = await buildFinalConfig(values, positionals, saved.tunnelConfig);
|
|
2741
|
-
finalConfig.configId = saved.configId;
|
|
2742
|
-
await startCli(finalConfig, manager);
|
|
2743
|
-
} else {
|
|
2744
|
-
await startNamedTunnels(resolved, manager);
|
|
2745
|
-
}
|
|
2746
|
-
}
|
|
2747
|
-
async function startAutoStartTunnels(manager) {
|
|
2748
|
-
const configs = getAutoStartConfigs();
|
|
2749
|
-
if (configs.length === 0) {
|
|
2750
|
-
printer_default.warn("No configs marked for auto-start. Use: pinggy config auto <name>");
|
|
2751
|
-
return;
|
|
2752
|
-
}
|
|
2753
|
-
printer_default.print(pico3.cyanBright(`Starting ${configs.length} auto-start tunnel(s)...`));
|
|
2754
|
-
for (const saved of configs) {
|
|
2755
|
-
await startSavedTunnel(saved, manager);
|
|
2756
|
-
}
|
|
2757
|
-
printer_default.print(pico3.gray("\nAll auto-start tunnels launched. Press Ctrl+C to stop.\n"));
|
|
2758
|
-
await new Promise(() => {
|
|
2759
|
-
});
|
|
2760
|
-
}
|
|
2761
|
-
async function startNamedTunnels(configs, manager) {
|
|
2762
|
-
printer_default.print(pico3.cyanBright(`Starting ${configs.length} tunnel(s)...`));
|
|
2763
|
-
for (const saved of configs) {
|
|
2764
|
-
await startSavedTunnel(saved, manager);
|
|
2765
|
-
}
|
|
2766
|
-
printer_default.print(pico3.gray("\nAll tunnels launched. Press Ctrl+C to stop.\n"));
|
|
2767
|
-
await new Promise(() => {
|
|
2768
|
-
});
|
|
2769
|
-
}
|
|
2770
|
-
async function startSavedTunnel(saved, manager) {
|
|
2771
|
-
const config = {
|
|
2772
|
-
...saved.tunnelConfig,
|
|
2773
|
-
configId: saved.configId,
|
|
2774
|
-
name: saved.name,
|
|
2775
|
-
optional: {
|
|
2776
|
-
...saved.tunnelConfig.optional,
|
|
2777
|
-
noTui: true
|
|
2778
|
-
}
|
|
2779
|
-
};
|
|
2780
|
-
try {
|
|
2781
|
-
const tunnel = await manager.createTunnel(config);
|
|
2782
|
-
await manager.startTunnel(tunnel.tunnelid);
|
|
2783
|
-
const urls = await manager.getTunnelUrls(tunnel.tunnelid);
|
|
2784
|
-
printer_default.success(`"${saved.name}" started`);
|
|
2785
|
-
(urls ?? []).forEach(
|
|
2786
|
-
(url) => printer_default.print(" " + pico3.magentaBright(url))
|
|
2787
|
-
);
|
|
2788
|
-
manager.registerWorkerErrorListner(tunnel.tunnelid, (_id, error) => {
|
|
2789
|
-
printer_default.error(`[${saved.name}] Fatal: ${error.message}`);
|
|
2790
|
-
});
|
|
2791
|
-
manager.registerDisconnectListener(tunnel.tunnelid, async (_id, error, messages) => {
|
|
2792
|
-
if (error) printer_default.warn(`[${saved.name}] Disconnected: ${error}`);
|
|
2793
|
-
messages?.forEach((m) => printer_default.warn(`[${saved.name}] ${m}`));
|
|
2794
|
-
});
|
|
2795
|
-
manager.registerReconnectingListener(tunnel.tunnelid, (_id, retryCnt) => {
|
|
2796
|
-
printer_default.print(pico3.gray(`[${saved.name}] Reconnecting (attempt #${retryCnt})...`));
|
|
2797
|
-
});
|
|
2798
|
-
manager.registerReconnectionCompletedListener(tunnel.tunnelid, async (_id, urls2) => {
|
|
2799
|
-
printer_default.success(`[${saved.name}] Reconnected`);
|
|
2800
|
-
(urls2 ?? []).forEach(
|
|
2801
|
-
(url) => printer_default.print(" " + pico3.magentaBright(url))
|
|
2802
|
-
);
|
|
2803
|
-
});
|
|
2804
|
-
manager.registerReconnectionFailedListener(tunnel.tunnelid, (_id, retryCnt) => {
|
|
2805
|
-
printer_default.error(`[${saved.name}] Reconnection failed after ${retryCnt} attempts`);
|
|
2806
|
-
});
|
|
2807
|
-
} catch (err) {
|
|
2808
|
-
printer_default.error(`[${saved.name}] Failed to start: ${err.message || err}`);
|
|
2809
|
-
}
|
|
2810
|
-
}
|
|
2811
|
-
function resolveConfig(nameOrId) {
|
|
2812
|
-
const saved = findConfig(nameOrId);
|
|
2813
|
-
if (!saved) {
|
|
2814
|
-
printer_default.error(`No config found matching "${nameOrId}". Use: pinggy config list`);
|
|
2815
|
-
return null;
|
|
2816
|
-
}
|
|
2817
|
-
return saved;
|
|
2818
|
-
}
|
|
2819
|
-
function requireName(args, command) {
|
|
2820
|
-
if (args.length === 0 || args[0].startsWith("-")) {
|
|
2821
|
-
printer_default.error(`Tunnel name is required. Usage: pinggy ${command} <name>`);
|
|
2822
|
-
process.exit(1);
|
|
2823
|
-
}
|
|
2824
|
-
return args[0];
|
|
2825
|
-
}
|
|
2826
|
-
function requireNames(args, command) {
|
|
2827
|
-
const names = [];
|
|
2828
|
-
for (const arg of args) {
|
|
2829
|
-
if (arg.startsWith("-")) break;
|
|
2830
|
-
names.push(arg);
|
|
2831
|
-
}
|
|
2832
|
-
if (names.length === 0) {
|
|
2833
|
-
printer_default.error(`At least one tunnel name is required. Usage: pinggy ${command} <name> [name2 ...]`);
|
|
2834
|
-
process.exit(1);
|
|
2835
|
-
}
|
|
2836
|
-
return names;
|
|
2837
|
-
}
|
|
2838
|
-
async function initRemoteManagementBackground(values) {
|
|
2839
|
-
const rmToken = values["remote-management"];
|
|
2840
|
-
if (typeof rmToken === "string" && rmToken.trim().length > 0) {
|
|
2841
|
-
const manageHost = values["manage"];
|
|
2842
|
-
try {
|
|
2843
|
-
await startRemoteManagement({
|
|
2844
|
-
apiKey: rmToken,
|
|
2845
|
-
serverUrl: buildRemoteManagementWsUrl(manageHost)
|
|
2846
|
-
});
|
|
2847
|
-
} catch (e) {
|
|
2848
|
-
logger.error("Failed to initiate remote management:", e);
|
|
2849
|
-
printer_default.fatal(e);
|
|
2850
|
-
}
|
|
2851
|
-
}
|
|
2852
|
-
}
|
|
2853
|
-
function printConfigHelp() {
|
|
2854
|
-
console.log("\nUsage: pinggy config <command> [name] [options]\n");
|
|
2855
|
-
console.log("Commands:");
|
|
2856
|
-
console.log(" list List all saved configs");
|
|
2857
|
-
console.log(" show <name> Show config details");
|
|
2858
|
-
console.log(" save <name> [tunnel flags] Save a tunnel config");
|
|
2859
|
-
console.log(" update <name> [tunnel flags] Update a saved config");
|
|
2860
|
-
console.log(" delete <name> Delete a saved config");
|
|
2861
|
-
console.log(" auto <name> Enable auto-start");
|
|
2862
|
-
console.log(" noauto <name> Disable auto-start\n");
|
|
2863
|
-
}
|
|
2864
|
-
function printStartHelp() {
|
|
2865
|
-
console.log("\nUsage: pinggy start <name> [options]\n");
|
|
2866
|
-
console.log("Examples:");
|
|
2867
|
-
console.log(" pinggy start my-tunnel Start a saved tunnel");
|
|
2868
|
-
console.log(" pinggy start my-tunnel -l 4000 Start with override");
|
|
2869
|
-
console.log(" pinggy start tunnela tunnelb Start multiple tunnels");
|
|
2870
|
-
console.log(" pinggy start --all Start all auto-start tunnels\n");
|
|
2871
|
-
}
|
|
2872
|
-
|
|
2873
|
-
// src/main.ts
|
|
2874
|
-
async function main() {
|
|
2875
|
-
try {
|
|
2876
|
-
const rawArgs = process.argv.slice(2);
|
|
2877
|
-
const manager = TunnelManager.getInstance();
|
|
2878
|
-
const gracefulShutdown = (signal) => {
|
|
2879
|
-
logger.info(`${signal} received: stopping tunnels and exiting`);
|
|
2880
|
-
console.log("\nStopping all tunnels...");
|
|
2881
|
-
manager.stopAllTunnels();
|
|
2882
|
-
console.log("Tunnels stopped. Exiting.");
|
|
2883
|
-
process.exit(0);
|
|
2884
|
-
};
|
|
2885
|
-
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
|
|
2886
|
-
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
|
|
2887
|
-
if (isSubcommand(rawArgs)) {
|
|
2888
|
-
await handleSubcommand(rawArgs, manager);
|
|
2889
|
-
return;
|
|
2890
|
-
}
|
|
2891
|
-
const { values, positionals, hasAnyArgs } = parseCliArgs(cliOptions);
|
|
2892
|
-
configureLogger(values);
|
|
2893
|
-
if (!hasAnyArgs || values.help) {
|
|
2894
|
-
printHelpMessage();
|
|
2895
|
-
return;
|
|
2896
|
-
}
|
|
2897
|
-
if (values.version) {
|
|
2898
|
-
printer_default.print(`Pinggy CLI version: ${getVersion()}`);
|
|
2899
|
-
return;
|
|
2900
|
-
}
|
|
2901
|
-
await buildAndStartTunnel(values, positionals, manager);
|
|
2902
|
-
} catch (error) {
|
|
2903
|
-
logger.error("Unhandled error in CLI:", error);
|
|
2904
|
-
printer_default.fatal(error);
|
|
2905
|
-
}
|
|
2906
|
-
}
|
|
2907
|
-
var currentFile = fileURLToPath(import.meta.url);
|
|
2908
|
-
var entryFile = null;
|
|
2909
|
-
try {
|
|
2910
|
-
entryFile = argv[1] ? realpathSync(argv[1]) : null;
|
|
2911
|
-
} catch (e) {
|
|
2912
|
-
entryFile = null;
|
|
2913
|
-
}
|
|
2914
|
-
if (entryFile && entryFile === currentFile) {
|
|
2915
|
-
main();
|
|
2916
|
-
}
|
|
2917
|
-
export {
|
|
2918
|
-
RemoteManagementUnauthorizedError,
|
|
2919
|
-
TunnelManager,
|
|
2920
|
-
TunnelOperations,
|
|
2921
|
-
closeRemoteManagement,
|
|
2922
|
-
enablePackageLogging,
|
|
2923
|
-
getRemoteManagementState,
|
|
2924
|
-
initiateRemoteManagement
|
|
2925
|
-
};
|