pinggy 0.1.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.
Files changed (68) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +182 -0
  3. package/dist/TunnelConfig.js +39 -0
  4. package/dist/TunnelManager.js +155 -0
  5. package/dist/cli/buildConfig.js +323 -0
  6. package/dist/cli/defaults.js +18 -0
  7. package/dist/cli/extendedOptions.js +134 -0
  8. package/dist/cli/help.js +37 -0
  9. package/dist/cli/options.js +36 -0
  10. package/dist/cli/remoteManagement.js +167 -0
  11. package/dist/cli/starCli.js +62 -0
  12. package/dist/cli/websocket_handlers.js +160 -0
  13. package/dist/index.js +60 -0
  14. package/dist/logger.js +67 -0
  15. package/dist/remote_management/handler.js +182 -0
  16. package/dist/remote_management/remoteManagement.js +159 -0
  17. package/dist/remote_management/remote_schema.js +122 -0
  18. package/dist/remote_management/websocket_handlers.js +163 -0
  19. package/dist/tui/asciArt.js +7 -0
  20. package/dist/tui/index.js +75 -0
  21. package/dist/tui/useTerminalSize.js +21 -0
  22. package/dist/tunnel_manager/TunnelManager.js +551 -0
  23. package/dist/tunnel_manager/handler.js +102 -0
  24. package/dist/tunnel_manager/remote_schema.js +106 -0
  25. package/dist/types.js +105 -0
  26. package/dist/utils/parseArgs.js +7 -0
  27. package/dist/utils/printer.js +68 -0
  28. package/dist/utils/util.js +3 -0
  29. package/jest.config.js +19 -0
  30. package/package.json +49 -0
  31. package/src/TunnelConfig.ts +40 -0
  32. package/src/_tests_/build_config.test.ts +91 -0
  33. package/src/cli/buildConfig.ts +357 -0
  34. package/src/cli/defaults.ts +20 -0
  35. package/src/cli/extendedOptions.ts +134 -0
  36. package/src/cli/help.ts +41 -0
  37. package/src/cli/options.ts +46 -0
  38. package/src/cli/starCli.tsx +118 -0
  39. package/src/cli/worker.ts +72 -0
  40. package/src/index.ts +65 -0
  41. package/src/logger.ts +86 -0
  42. package/src/remote_management/handler.ts +199 -0
  43. package/src/remote_management/remoteManagement.ts +168 -0
  44. package/src/remote_management/remote_schema.ts +134 -0
  45. package/src/remote_management/websocket_handlers.ts +166 -0
  46. package/src/tui/asciArt.ts +7 -0
  47. package/src/tui/hooks/useQrCodes.ts +27 -0
  48. package/src/tui/hooks/useReqResHeaders.ts +27 -0
  49. package/src/tui/hooks/useTerminalSize.ts +26 -0
  50. package/src/tui/hooks/useTerminalStats.ts +24 -0
  51. package/src/tui/hooks/useWebDebugger.ts +98 -0
  52. package/src/tui/index.tsx +221 -0
  53. package/src/tui/layout/Borders.tsx +15 -0
  54. package/src/tui/layout/Container.tsx +15 -0
  55. package/src/tui/sections/DebuggerDetailModal.tsx +53 -0
  56. package/src/tui/sections/KeyBindings.tsx +58 -0
  57. package/src/tui/sections/QrCodeSection.tsx +28 -0
  58. package/src/tui/sections/StatsSection.tsx +20 -0
  59. package/src/tui/sections/URLsSection.tsx +53 -0
  60. package/src/tui/utils/utils.ts +35 -0
  61. package/src/tunnel_manager/TunnelManager.ts +646 -0
  62. package/src/types.ts +234 -0
  63. package/src/utils/getFreePort.ts +41 -0
  64. package/src/utils/parseArgs.ts +29 -0
  65. package/src/utils/printer.ts +79 -0
  66. package/src/utils/util.ts +13 -0
  67. package/tsconfig.jest.json +8 -0
  68. package/tsconfig.json +17 -0
@@ -0,0 +1,357 @@
1
+ import { defaultOptions } from "./defaults.js";
2
+ import { parseExtendedOptions } from "./extendedOptions.js";
3
+ import { logger } from "../logger.js";
4
+ import { FinalConfig, Forwarding } from "../types.js";
5
+ import { ParsedValues } from "../utils/parseArgs.js";
6
+ import { cliOptions } from "./options.js";
7
+ import { isValidPort } from "../utils/util.js";
8
+ import { v4 as uuidv4 } from "uuid";
9
+ import { TunnelType } from "@pinggy/pinggy";
10
+ import fs from "fs";
11
+ import path from "path";
12
+
13
+
14
+ const domainRegex = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
15
+
16
+ function parseUserAndDomain(str: string) {
17
+ let token: string | undefined;
18
+ let type: string | undefined;
19
+ let server: string | undefined;
20
+ let qrCode: boolean | undefined;
21
+
22
+ if (!str) return { token, type, server, qrCode } as const;
23
+
24
+ if (str.includes('@')) {
25
+ const [user, domain] = str.split('@', 2);
26
+ if (domainRegex.test(domain)) {
27
+ server = domain;
28
+ // parse user modifiers like token+type or just type
29
+ const parts = user.split('+');
30
+ for (const part of parts) {
31
+ if ([TunnelType.Http, TunnelType.Tcp, TunnelType.Tls, TunnelType.Udp, TunnelType.TlsTcp].includes(part.toLowerCase() as typeof TunnelType[keyof typeof TunnelType])) {
32
+ type = part;
33
+ } else if (part === 'force') {
34
+ token = (token ? token + '+' : '') + part;
35
+ } else if (part === 'qr') {
36
+ qrCode = true;
37
+ } else {
38
+ token = (token ? token + '+' : '') + part;
39
+ }
40
+ }
41
+ }
42
+ } else if (domainRegex.test(str)) {
43
+ server = str;
44
+ }
45
+ return { token, type, server, qrCode } as const;
46
+ }
47
+
48
+ function parseUsers(positionalArgs: string[], explicitToken?: string) {
49
+ let token: string | undefined;
50
+ let server: string | undefined;
51
+ let type: string | undefined;
52
+ let forceFlag = false;
53
+ let qrCode = false;
54
+ let remaining: string[] = [...positionalArgs];
55
+
56
+ // Allow explicit token to carry user@domain
57
+ if (typeof explicitToken === 'string') {
58
+ const parsed = parseUserAndDomain(explicitToken);
59
+ if (parsed.server) server = parsed.server;
60
+ if (parsed.type) type = parsed.type;
61
+ if (parsed.token) token = parsed.token;
62
+ }
63
+
64
+ if (remaining.length > 0) {
65
+ const first = remaining[0];
66
+ const parsed = parseUserAndDomain(first);
67
+ if (parsed.server) {
68
+ server = parsed.server;
69
+ if (parsed.type) type = parsed.type;
70
+ if (parsed.token) {
71
+ if (parsed.token.includes('+')) {
72
+ const parts = parsed.token.split('+');
73
+ const tOnly = parts.filter((p) => p !== 'force').join('+');
74
+ if (tOnly) token = tOnly;
75
+ if (parts.includes('force')) forceFlag = true;
76
+ } else {
77
+ token = parsed.token;
78
+ }
79
+ } if (parsed.qrCode) {
80
+ // QR code request detected
81
+ qrCode = true;
82
+ }
83
+ remaining = remaining.slice(1);
84
+ }
85
+ }
86
+
87
+ return { token, server, type, forceFlag, qrCode, remaining } as const;
88
+ }
89
+
90
+ function parseType(finalConfig: FinalConfig, values: ParsedValues<typeof cliOptions>, inferredType?: string) {
91
+ const t = inferredType || values.type || finalConfig.tunnelType;
92
+ if (t === TunnelType.Http || t === TunnelType.Tcp || t === TunnelType.Tls || t === TunnelType.Udp) {
93
+ finalConfig.tunnelType = [t];
94
+ }
95
+ }
96
+
97
+ function parseLocalPort(finalConfig: FinalConfig, values: ParsedValues<typeof cliOptions>): Error | null {
98
+ if (typeof values.localport !== 'string') return null;
99
+ let lp = values.localport.trim();
100
+
101
+ let isHttps = false;
102
+ if (lp.startsWith('https://')) {
103
+ isHttps = true;
104
+ lp = lp.replace(/^https:\/\//, '');
105
+ } else if (lp.startsWith('http://')) {
106
+ lp = lp.replace(/^http:\/\//, '');
107
+ }
108
+
109
+ const parts = lp.split(':');
110
+ if (parts.length === 1) {
111
+ const port = parseInt(parts[0], 10);
112
+ if (!Number.isNaN(port) && isValidPort(port)) {
113
+ finalConfig.forwarding = `localhost:${port}`;
114
+ } else {
115
+ return new Error('Invalid local port');
116
+ }
117
+ } else if (parts.length === 2) {
118
+ const host = parts[0] || 'localhost';
119
+ const port = parseInt(parts[1], 10);
120
+ if (!Number.isNaN(port) && isValidPort(port)) {
121
+ finalConfig.forwarding = `${host}:${port}`;
122
+ } else {
123
+ return new Error('Invalid local port. Please use -h option for help.');
124
+ }
125
+ } else {
126
+ return new Error('Invalid --localport format. Please use -h option for help.');
127
+ }
128
+
129
+ return null;
130
+ }
131
+
132
+ // Remove IPv6 Brackets
133
+ // Example: From [::1] to ::1
134
+ function removeIPv6Brackets(ip: string): string {
135
+ if (ip.startsWith("[") && ip.endsWith("]")) {
136
+ return ip.slice(1, -1);
137
+ }
138
+ return ip;
139
+ }
140
+
141
+ function ipv6SafeSplitColon(s: string): string[] {
142
+ const result: string[] = [];
143
+ let buf = "";
144
+ const stack: string[] = [];
145
+
146
+ for (let i = 0; i < s.length; i++) {
147
+ const c = s[i];
148
+
149
+ if (c === "[") {
150
+ stack.push(c);
151
+ } else if (c === "]" && stack.length > 0) {
152
+ stack.pop();
153
+ }
154
+
155
+ if (c === ":" && stack.length === 0) {
156
+ result.push(buf);
157
+ buf = "";
158
+ } else {
159
+ buf += c;
160
+ }
161
+ }
162
+
163
+ result.push(buf);
164
+ return result;
165
+ }
166
+
167
+ function parseForwarding(forwarding: string): Forwarding | Error {
168
+ const parts = ipv6SafeSplitColon(forwarding);
169
+
170
+ // Format: 5555:localhost:6666
171
+ if (parts.length === 3) {
172
+ const remotePort = parseInt(parts[0], 10);
173
+ const localDomain = removeIPv6Brackets(parts[1] || "localhost");
174
+ const localPort = parseInt(parts[2], 10);
175
+ return { remotePort, localDomain, localPort };
176
+ }
177
+
178
+ // Format: domain.com:5555:localhost:6666
179
+ if (parts.length === 4) {
180
+ const remoteDomain = removeIPv6Brackets(parts[0]);
181
+ const remotePort = parseInt(parts[1], 10);
182
+ const localDomain = removeIPv6Brackets(parts[2] || "localhost");
183
+ const localPort = parseInt(parts[3], 10);
184
+ return { remoteDomain, remotePort, localDomain, localPort };
185
+ }
186
+
187
+ return new Error("forwarding address incorrect");
188
+ }
189
+
190
+ function parseReverseTunnelAddr(finalConfig: FinalConfig, values: ParsedValues<typeof cliOptions>): Error | null {
191
+ const reverseTunnel = values.R;
192
+
193
+ if (!Array.isArray(reverseTunnel) || reverseTunnel.length === 0) {
194
+ return new Error("local port not specified. Please use '-h' option for help.");
195
+ }
196
+ const forwarding = parseForwarding(reverseTunnel[0]);
197
+ if (forwarding instanceof Error) {
198
+ return forwarding;
199
+ }
200
+ finalConfig.forwarding = `${forwarding.localDomain}:${forwarding.localPort}`;
201
+ // Additional forwarding
202
+ if (reverseTunnel.length > 1) {
203
+ finalConfig.additionalForwarding = []
204
+ for (const t of reverseTunnel.slice(1)) {
205
+ const f = parseForwarding(t);
206
+ if (f instanceof Error) {
207
+ return f;
208
+ }
209
+ finalConfig.additionalForwarding.push(f);
210
+ }
211
+ }
212
+ return null
213
+
214
+ }
215
+
216
+ function parseLocalTunnelAddr(finalConfig: FinalConfig, values: ParsedValues<typeof cliOptions>) {
217
+ if (!Array.isArray(values.L) || values.L.length === 0) return null;
218
+ const firstL = values.L[0] as string;
219
+ const parts = firstL.split(':');
220
+ if (parts.length === 3) {
221
+ const lp = parseInt(parts[2], 10);
222
+ if (!Number.isNaN(lp) && isValidPort(lp)) {
223
+ finalConfig.webDebugger = `localhost:${lp}`;
224
+ } else {
225
+ return new Error(`Invalid debugger port ${lp}`);
226
+
227
+ }
228
+ } else {
229
+ return new Error("Incorrect command line arguments: web debugger address incorrect. Please use '-h' option for help.");
230
+
231
+ }
232
+ }
233
+
234
+ function parseDebugger(finalConfig: FinalConfig, values: ParsedValues<typeof cliOptions>) {
235
+ let dbg = values.debugger;
236
+ if (typeof dbg !== 'string') return;
237
+ dbg = dbg.startsWith(':') ? dbg.slice(1) : dbg;
238
+ const d = parseInt(dbg, 10);
239
+ if (!Number.isNaN(d) && isValidPort(d)) {
240
+ finalConfig.webDebugger = `localhost:${d}`;
241
+ } else {
242
+ logger.error('Invalid debugger port:', dbg);
243
+ return new Error(`Invalid debugger port ${dbg}. Please use '-h' option for help.`);
244
+ }
245
+ }
246
+
247
+ function parseToken(finalConfig: FinalConfig, explicitToken?: string) {
248
+ if (typeof explicitToken === 'string' && explicitToken) {
249
+ finalConfig.token = explicitToken;
250
+ }
251
+ }
252
+
253
+
254
+ function parseArgs(finalConfig: FinalConfig, remainingPositionals: string[]) {
255
+ parseExtendedOptions(remainingPositionals, finalConfig);
256
+ }
257
+
258
+ function storeJson(config: FinalConfig, saveconf: string | null) {
259
+ if (saveconf) {
260
+ const path = saveconf;
261
+ try {
262
+ fs.writeFileSync(path, JSON.stringify(config, null, 2), { encoding: 'utf-8', flag: 'w' });
263
+ logger.info(`Configuration saved to ${path}`);
264
+ } catch (err) {
265
+ const msg = err instanceof Error ? err.message : String(err);
266
+ logger.error("Error loading configuration:", msg);
267
+ }
268
+
269
+ }
270
+ }
271
+
272
+ function loadJsonConfig(config: ParsedValues<typeof cliOptions>): FinalConfig | null {
273
+ const configpath = config["conf"];
274
+ if (typeof configpath === "string" && configpath.trim().length > 0) {
275
+ const filepath = path.resolve(configpath);
276
+ try {
277
+ const data = fs.readFileSync(filepath, { encoding: 'utf-8' });
278
+ const json = JSON.parse(data);
279
+ return json;
280
+ } catch (err) {
281
+ logger.error("Error loading configuration:", err);
282
+ }
283
+
284
+ }
285
+ return null;
286
+
287
+ }
288
+
289
+ function isSaveConfOption(values: ParsedValues<typeof cliOptions>): string | null {
290
+ const saveconf = values["saveconf"];
291
+ if (typeof saveconf === "string" && saveconf.trim().length > 0) {
292
+ return saveconf;
293
+ }
294
+ return null;
295
+ }
296
+
297
+ export function buildFinalConfig(values: ParsedValues<typeof cliOptions>, positionals: string[]): FinalConfig {
298
+ let token: string | undefined;
299
+ let server: string | undefined;
300
+ let type: string | undefined;
301
+ let forceFlag = false;
302
+ let qrCode = false;
303
+ let finalConfig = new Object() as FinalConfig;
304
+ let saveconf = isSaveConfOption(values);
305
+
306
+ const configFromFile = loadJsonConfig(values);
307
+ if (configFromFile !== null) {
308
+ finalConfig = { ...configFromFile };
309
+ }
310
+
311
+ const userParse = parseUsers(positionals, values.token);
312
+ token = userParse.token;
313
+ server = userParse.server;
314
+ type = userParse.type;
315
+ forceFlag = userParse.forceFlag;
316
+ qrCode = userParse.qrCode;
317
+ const remainingPositionals: string[] = userParse.remaining;
318
+
319
+ const initialTunnel = (type || values.type) as TunnelType;
320
+ finalConfig = {
321
+ ...defaultOptions,
322
+ configid: uuidv4(),
323
+ token: token || (typeof values.token === 'string' ? values.token : ''),
324
+ serverAddress: server || defaultOptions.serverAddress,
325
+ tunnelType: initialTunnel ? [initialTunnel] : defaultOptions.tunnelType,
326
+ NoTUI: values.NoTUI || false,
327
+ qrCode: qrCode || false,
328
+ };
329
+
330
+
331
+ parseType(finalConfig, values, type);
332
+
333
+ // Apply token
334
+ parseToken(finalConfig, token || values.token);
335
+
336
+ const dbgErr = parseDebugger(finalConfig, values);
337
+ if (dbgErr instanceof Error) throw dbgErr;
338
+
339
+ const lpErr = parseLocalPort(finalConfig, values);
340
+ if (lpErr instanceof Error) throw lpErr;
341
+
342
+ const rErr = parseReverseTunnelAddr(finalConfig, values);
343
+ if (rErr instanceof Error) throw rErr;
344
+
345
+ const lErr = parseLocalTunnelAddr(finalConfig, values);
346
+ if (lErr instanceof Error) throw lErr;
347
+
348
+ // Apply force flag if indicated via user
349
+ if (forceFlag) finalConfig.force = true;
350
+
351
+ // Parse positional extended options (like x:, w:, b:, k:, a:, u:, r:)
352
+ parseArgs(finalConfig, remainingPositionals);
353
+
354
+ storeJson(finalConfig, saveconf);
355
+
356
+ return finalConfig;
357
+ }
@@ -0,0 +1,20 @@
1
+ import { PinggyOptions, TunnelType } from "@pinggy/pinggy";
2
+
3
+ // Default configuration for Tunnel
4
+ export const defaultOptions: Omit<PinggyOptions, 'token'> & { token: string | undefined } = {
5
+ token: undefined, // No default token
6
+ serverAddress: "a.pinggy.io",
7
+ forwarding: "localhost:8000",
8
+ webDebugger: "",
9
+ tunnelType: [TunnelType.Http],
10
+ ipWhitelist: [],
11
+ basicAuth: [],
12
+ bearerTokenAuth: [],
13
+ headerModification: [],
14
+ force: true,
15
+ xForwardedFor: false,
16
+ httpsOnly: false,
17
+ originalRequestUrl: false,
18
+ allowPreflight: false,
19
+ reverseProxy: false,
20
+ };
@@ -0,0 +1,134 @@
1
+ import { PinggyOptions } from "@pinggy/pinggy";
2
+ import { isIP } from 'net';
3
+ import { logger } from "../logger.js";
4
+ import CLIPrinter from "../utils/printer.js";
5
+
6
+ export function parseExtendedOptions(options: string[] | undefined, config: PinggyOptions) {
7
+ if (!options) return;
8
+
9
+ for (const opt of options) {
10
+ const [key, value] = opt.replace(/^"|"$/g, "").split(/:(.+)/).filter(Boolean);
11
+
12
+ switch (key) {
13
+ case "x":
14
+ switch (value) {
15
+ case "https":
16
+ case "httpsonly":
17
+ config.httpsOnly = true;
18
+ break;
19
+
20
+ case "passpreflight":
21
+ case "allowpreflight":
22
+ config.allowPreflight = true;
23
+ break;
24
+
25
+ case "reverseproxy":
26
+ config.reverseProxy = false;
27
+ break;
28
+
29
+ case "xff":
30
+ config.xForwardedFor = true;
31
+ break;
32
+
33
+ case "fullurl":
34
+ case "fullrequesturl":
35
+ config.originalRequestUrl = true;
36
+ break;
37
+
38
+ default:
39
+ CLIPrinter.warn(`Unknown extended option "${key}"`);
40
+ logger.warn(`Warning: Unknown extended option "${key}"`);
41
+ break;
42
+ }
43
+ break;
44
+ case "w":
45
+ // Whitelist IPs
46
+ if (value) {
47
+ const ips = value.split(",").map(ip => ip.trim()).filter(Boolean);
48
+ const invalidIps = ips.filter(ip => !isValidIpV4Cidr(ip));
49
+
50
+ if (invalidIps.length > 0) {
51
+ CLIPrinter.warn(`Invalid IP/CIDR(s) in whitelist: ${invalidIps.join(", ")}`);
52
+ logger.warn(`Warning: Invalid IP/CIDR(s) in whitelist: ${invalidIps.join(", ")}`);
53
+ }
54
+ if (!(invalidIps.length > 0)) {
55
+ config.ipWhitelist = ips;
56
+ }
57
+ } else {
58
+ CLIPrinter.warn(`Extended option "${opt}" for 'w' requires IP(s)`);
59
+ logger.warn(`Warning: Extended option "${opt}" for 'w' requires IP(s)`);
60
+ }
61
+ break;
62
+ case "k":
63
+ //bearer tokens
64
+ if (!config.bearerTokenAuth) config.bearerTokenAuth = [];
65
+ if (value) {
66
+ config.bearerTokenAuth.push(value);
67
+ } else {
68
+ CLIPrinter.warn(`Extended option "${opt}" for 'k' requires a value`);
69
+ logger.warn(`Warning: Extended option "${opt}" for 'k' requires a value`);
70
+ }
71
+ break;
72
+
73
+ case "b":
74
+ // basicauth "username:password"
75
+ if (value && value.includes(":")) {
76
+ const [username, password] = value.split(/:(.+)/);
77
+ if (!config.basicAuth) config.basicAuth = [];
78
+ config.basicAuth.push({ username, password });
79
+ } else {
80
+ CLIPrinter.warn(`Extended option "${opt}" for 'b' requires value in format username:password`);
81
+ logger.warn(`Warning: Extended option "${opt}" for 'b' requires value in format username:password`);
82
+ }
83
+ break;
84
+
85
+ case "a":
86
+ // Add header
87
+ if (value && value.includes(":")) {
88
+ const [key, val] = value.split(/:(.+)/);
89
+ if (!config.headerModification) config.headerModification = [];
90
+ config.headerModification.push({ type: "add", key, value: [val] });
91
+ } else {
92
+ CLIPrinter.warn(`Extended option "${opt}" for 'a' requires key:value`);
93
+ logger.warn(`Warning: Extended option "${opt}" for 'a' requires key:value`);
94
+ }
95
+ break;
96
+ case "u":
97
+ // Update header
98
+ if (value && value.includes(":")) {
99
+ const [key, val] = value.split(/:(.+)/);
100
+ if (!config.headerModification) config.headerModification = [];
101
+ config.headerModification.push({ type: "update", key, value: [val] });
102
+ } else {
103
+ CLIPrinter.warn(`Extended option "${opt}" for 'u' requires key:value`);
104
+ logger.warn(`Warning: Extended option "${opt}" for 'u' requires key:value`);
105
+ }
106
+ break;
107
+ case "r":
108
+ // Remove header
109
+ if (value) {
110
+ if (!config.headerModification) config.headerModification = [];
111
+ config.headerModification.push({ type: "remove", key: value });
112
+ } else {
113
+ CLIPrinter.warn(`Extended option "${opt}" for 'r' requires a key`);
114
+ }
115
+ break;
116
+ default:
117
+ CLIPrinter.warn(`Unknown extended option "${key}"`);
118
+ break;
119
+ }
120
+ }
121
+ }
122
+
123
+ function isValidIpV4Cidr(input: string): boolean {
124
+ // Check for CIDR notation
125
+ if (input.includes('/')) {
126
+ const [ip, mask] = input.split('/');
127
+ if (!ip || !mask) return false;
128
+ const isIp4 = isIP(ip) === 4;
129
+ const maskNum = parseInt(mask, 10);
130
+ const isMaskValid = !isNaN(maskNum) && maskNum >= 0 && maskNum <= 32;
131
+ return isIp4 && isMaskValid;
132
+ }
133
+ return false;
134
+ }
@@ -0,0 +1,41 @@
1
+ import { cliOptions } from "./options.js";
2
+
3
+ export function printHelpMessage() {
4
+ console.log("\nPinggy CLI Tool - Create secure tunnels to your localhost.");
5
+ console.log("\nUsage:");
6
+ console.log(" pinggy [options] [user@domain] # Domain can be any valid domain\n");
7
+
8
+ console.log("Options:");
9
+ for (const [key, value] of Object.entries(cliOptions)) {
10
+ if ((value as any).hidden) continue;
11
+ const short = 'short' in value && (value as any).short ? `-${(value as any).short}, ` : ' ';
12
+ const optType = (value as any).type === 'boolean' ? '' : '<value>';
13
+ console.log(` ${short}--${key.padEnd(17)} ${optType.padEnd(8)} ${(value as any).description}`);
14
+ }
15
+
16
+ console.log("\nExtended options :");
17
+ console.log(" x:https Enforce HTTPS only (redirect HTTP to HTTPS)");
18
+ console.log(" x:noreverseproxy Disable built-in reverse-proxy header injection");
19
+ console.log(" x:localservertls:host Connect to local HTTPS server with SNI");
20
+ console.log(" x:passpreflight Pass CORS preflight requests unchanged");
21
+ console.log(" a:Key:Val Add header");
22
+ console.log(" u:Key:Val Update header");
23
+ console.log(" r:Key Remove header");
24
+ console.log(" b:user:pass Basic auth");
25
+ console.log(" k:BEARER Bearer token");
26
+ console.log(" w:192.168.1.0/24 IP whitelist (CIDR)\n");
27
+
28
+ console.log("Examples (SSH-style):");
29
+ console.log(" pinggy -R0:localhost:3000 # Basic HTTP tunnel");
30
+ console.log(" pinggy --type tcp -R0:localhost:22 # TCP tunnel for SSH");
31
+ console.log(" pinggy -R0:localhost:8080 -L4300:localhost:4300 # HTTP tunnel with debugger");
32
+ console.log(" pinggy tcp@ap.example.com -R0:localhost:22 # TCP tunnel to region\n");
33
+
34
+ console.log("Examples (User-friendly):");
35
+ console.log(" pinggy -p 3000 # Basic HTTP tunnel");
36
+ console.log(" pinggy --type tcp -p 22 # TCP tunnel for SSH");
37
+ console.log(" pinggy -l 8080 -d 4300 # HTTP tunnel with debugger");
38
+ console.log(" pinggy mytoken@a.example.com -p 3000 # Authenticated tunnel");
39
+ console.log(" pinggy x:https x:xff -l https://localhost:8443 # HTTPS-only + XFF");
40
+ console.log(" pinggy w:192.168.1.0/24 -l 8080 # IP whitelist restriction");
41
+ }
@@ -0,0 +1,46 @@
1
+ export const cliOptions = {
2
+ // SSH-like options
3
+ R: { type: 'string' as const, multiple: true, description: 'Local port. Eg. -R0:localhost:3000 will forward tunnel connections to local port 3000.' },
4
+ L: { type: 'string' as const, multiple: true, description: 'Web Debugger address. Eg. -L4300:localhost:4300 will start web debugger on port 4300.' },
5
+ o: { type: 'string' as const, multiple: true, description: 'Options', hidden: true },
6
+ 'server-port': { type: 'string' as const, short: 'p', description: 'Pinggy server port. Default: 443' },
7
+
8
+ v4: { type: 'boolean' as const, short: '4', description: 'IPv4 only', hidden: true },
9
+ v6: { type: 'boolean' as const, short: '6', description: 'IPv6 only', hidden: true },
10
+
11
+ // These options appear in the ssh command, but we ignore it in CLI
12
+ t: { type: 'boolean' as const, description: 'hidden', hidden: true },
13
+ T: { type: 'boolean' as const, description: 'hidden', hidden: true },
14
+ n: { type: 'boolean' as const, description: 'hidden', hidden: true },
15
+ N: { type: 'boolean' as const, description: 'hidden', hidden: true },
16
+
17
+ // Better options
18
+ type: { type: 'string' as const, description: 'Type of the connection. Eg. --type tcp' },
19
+ localport: { type: 'string' as const, short: 'l', description: 'Takes input as [protocol:][host:]port. Eg. --localport https://localhost:8000 OR -l 3000' },
20
+ debugger: { type: 'string' as const, short: 'd', description: 'Port for web debugger. Eg. --debugger 4300 OR -d 4300' },
21
+ token: { type: 'string' as const, description: 'Token for authentication. Eg. --token TOKEN_VALUE' },
22
+
23
+ // Logging options (CLI overrides env)
24
+ loglevel: { type: 'string' as const, description: 'Logging level: ERROR, INFO, DEBUG. Overrides PINGGY_LOG_LEVEL environment variable' },
25
+ logfile: { type: 'string' as const, description: 'Path to log file. Overrides PINGGY_LOG_FILE environment variable' },
26
+ printlog: { type: 'boolean' as const, short: 'g', description: 'Also print logs to stdout. Overrides PINGGY_LOG_STDOUT environment variable' },
27
+
28
+ // Save and load config
29
+ saveconf: { type: 'string' as const, description: 'Create the configuration file based on the options provided here' },
30
+ conf: { type: 'string' as const, description: 'Use the configuration file as base. Other options will be used to override this file' },
31
+
32
+ // File server
33
+ serve: { type: 'string' as const, description: 'Start a webserver to serve files from the specified path. Eg --serve /path/to/files' },
34
+
35
+ // Remote Control
36
+ 'remote-management': { type: 'string' as const, description: 'Enable remote management of tunnels with token. Eg. --remote-management API_KEY' },
37
+ manage: { type: 'string' as const, description: 'Provide a server address to manage tunnels. Eg --manage dashboard.pinggy.io' },
38
+ NoTUI:{ type: 'boolean' as const, description: 'Disable TUI in remote management mode'},
39
+ // Misc
40
+ version: { type: 'boolean' as const, description: 'Print version' },
41
+
42
+ // Help
43
+ help: { type: 'boolean' as const, short: 'h', description: 'Show this help message' },
44
+ } as const;
45
+
46
+ export type CliOptions = typeof cliOptions;