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,182 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { ErrorCode, newErrorResponse, newStatus, TunnelErrorCodeType, newStats } from "../types.js";
11
+ import { TunnelManager } from "../tunnel_manager/TunnelManager.js";
12
+ import { pinggyOptionsToTunnelConfig, tunnelConfigToPinggyOptions } from "./remote_schema.js";
13
+ export class TunnelOperations {
14
+ constructor() {
15
+ this.tunnelManager = TunnelManager.getInstance(); // Use singleton instance
16
+ }
17
+ handleStart(config) {
18
+ try {
19
+ // Convert TunnelConfig -> PinggyOptions
20
+ const pinggyOpts = tunnelConfigToPinggyOptions(config);
21
+ const tunnelResp = this.tunnelManager.createTunnel(Object.assign(Object.assign({}, pinggyOpts), { configid: config.configid, tunnelName: config.configname }));
22
+ this.tunnelManager.startTunnel(tunnelResp.tunnelid);
23
+ const tunnelPconfig = this.tunnelManager.getTunnelConfig("", tunnelResp.tunnelid);
24
+ const tunnelGreetMsg = this.tunnelManager.getTunnelGreetMessage(tunnelResp.tunnelid);
25
+ const tlsInfo = this.tunnelManager.getLocalserverTlsInfo(tunnelResp.tunnelid);
26
+ // Construct TunnelResponse object
27
+ const tunnelStatus = {
28
+ tunnelid: tunnelResp.tunnelid,
29
+ remoteurls: [],
30
+ tunnelconfig: pinggyOptionsToTunnelConfig(tunnelPconfig, config.configid, tunnelResp.tunnelName, tlsInfo, tunnelGreetMsg),
31
+ status: newStatus(this.tunnelManager.getTunnelStatus(tunnelResp.tunnelid), TunnelErrorCodeType.NoError, ""),
32
+ stats: tunnelResp.instance.getLatestUsage()
33
+ };
34
+ return tunnelStatus;
35
+ }
36
+ catch (error) {
37
+ return newErrorResponse({
38
+ code: ErrorCode.ErrorStartingTunnel,
39
+ message: error instanceof Error ? error.message : 'Unknown error occurred while starting tunnel'
40
+ });
41
+ }
42
+ }
43
+ handleUpdateConfig(config) {
44
+ return __awaiter(this, void 0, void 0, function* () {
45
+ try {
46
+ // Convert TunnelConfig -> PinggyOptions
47
+ const pinggyOpts = tunnelConfigToPinggyOptions(config);
48
+ const managedTunnel = yield this.tunnelManager.updateConfig(Object.assign(Object.assign({}, pinggyOpts), { configid: config.configid, tunnelName: config.configname }));
49
+ if (!managedTunnel.tunnelConfig) {
50
+ throw new Error("Failed to update tunnel configuration");
51
+ }
52
+ if (!managedTunnel.instance) {
53
+ throw new Error("Tunnel instance not found after configuration update");
54
+ }
55
+ const tunnelStatus = {
56
+ tunnelid: managedTunnel.tunnelid,
57
+ remoteurls: [],
58
+ tunnelconfig: pinggyOptionsToTunnelConfig(managedTunnel.tunnelConfig, config.configid, managedTunnel.tunnelName, this.tunnelManager.getLocalserverTlsInfo(managedTunnel.tunnelid), this.tunnelManager.getTunnelGreetMessage(managedTunnel.tunnelid)),
59
+ status: newStatus(this.tunnelManager.getTunnelStatus(managedTunnel.tunnelid), TunnelErrorCodeType.NoError, ""),
60
+ stats: managedTunnel.instance.getLatestUsage()
61
+ };
62
+ return tunnelStatus;
63
+ }
64
+ catch (error) {
65
+ return newErrorResponse({
66
+ code: ErrorCode.InternalServerError,
67
+ message: error instanceof Error ? error.message : 'Failed to update tunnel configuration'
68
+ });
69
+ }
70
+ });
71
+ }
72
+ handleList() {
73
+ try {
74
+ const tunnels = this.tunnelManager.getAllTunnels();
75
+ const result = tunnels.map(tunnel => {
76
+ // try to get stats from manager first
77
+ const statsFromManager = this.tunnelManager.getTunnelStats(tunnel.tunnelid);
78
+ let stats = statsFromManager;
79
+ // if null/undefined, return default stats
80
+ if (!stats) {
81
+ stats = newStats();
82
+ }
83
+ return {
84
+ tunnelid: tunnel.tunnelid,
85
+ remoteurls: tunnel.remoteurls,
86
+ status: newStatus(this.tunnelManager.getTunnelStatus(tunnel.tunnelid), TunnelErrorCodeType.NoError, ""),
87
+ stats: stats,
88
+ // Convert PinggyOptions -> TunnelConfig
89
+ tunnelconfig: pinggyOptionsToTunnelConfig(this.tunnelManager.getTunnelConfig("", tunnel.tunnelid), tunnel.configid, tunnel.tunnelName, this.tunnelManager.getLocalserverTlsInfo(tunnel.tunnelid), this.tunnelManager.getTunnelGreetMessage(tunnel.tunnelid))
90
+ };
91
+ });
92
+ return result;
93
+ }
94
+ catch (error) {
95
+ return newErrorResponse({
96
+ code: ErrorCode.InternalServerError,
97
+ message: error instanceof Error ? error.message : 'Failed to list tunnels'
98
+ });
99
+ }
100
+ }
101
+ handleStop(tunnelid) {
102
+ try {
103
+ const { configid, tunnelid: stoppedTunnelId } = this.tunnelManager.stopTunnel(tunnelid);
104
+ const tunnelInstance = this.tunnelManager.getManagedTunnel("", tunnelid);
105
+ if (!tunnelInstance) {
106
+ throw new Error(`Tunnel instance for ID "${tunnelid}" not found`);
107
+ }
108
+ if (!tunnelInstance.tunnelConfig) {
109
+ throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
110
+ }
111
+ const tunnelResponse = {
112
+ tunnelid: stoppedTunnelId,
113
+ remoteurls: [],
114
+ tunnelconfig: pinggyOptionsToTunnelConfig(tunnelInstance.tunnelConfig, configid, tunnelInstance.tunnelName, this.tunnelManager.getLocalserverTlsInfo(tunnelid), this.tunnelManager.getTunnelGreetMessage(tunnelid)),
115
+ status: newStatus(this.tunnelManager.getTunnelStatus(stoppedTunnelId), TunnelErrorCodeType.NoError, ""),
116
+ stats: newStats()
117
+ };
118
+ return tunnelResponse;
119
+ }
120
+ catch (error) {
121
+ return newErrorResponse({
122
+ code: ErrorCode.TunnelNotFound,
123
+ message: error instanceof Error ? error.message : 'Failed to stop tunnel'
124
+ });
125
+ }
126
+ }
127
+ handleGet(tunnelid) {
128
+ try {
129
+ const tunnelState = this.tunnelManager.getTunnelStatus(tunnelid);
130
+ const tunnelInstance = this.tunnelManager.getManagedTunnel("", tunnelid);
131
+ if (!tunnelInstance) {
132
+ throw new Error(`Tunnel instance for ID "${tunnelid}" not found`);
133
+ }
134
+ if (!tunnelInstance.tunnelConfig) {
135
+ throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
136
+ }
137
+ // TODO: directly calling to getLatestUsage() may through error if tunnel stopped earlier
138
+ const stats = this.tunnelManager.getTunnelStats(tunnelid) ? this.tunnelManager.getTunnelStats(tunnelid) : tunnelInstance.instance.getLatestUsage();
139
+ const tunnelResponse = {
140
+ tunnelid: tunnelid,
141
+ remoteurls: this.tunnelManager.getTunnelUrls(tunnelid),
142
+ status: newStatus(tunnelState, TunnelErrorCodeType.NoError, ""),
143
+ stats: stats,
144
+ tunnelconfig: pinggyOptionsToTunnelConfig(tunnelInstance.tunnelConfig, tunnelInstance.configid, tunnelInstance.tunnelName, this.tunnelManager.getLocalserverTlsInfo(tunnelid), this.tunnelManager.getTunnelGreetMessage(tunnelid))
145
+ };
146
+ return tunnelResponse;
147
+ }
148
+ catch (error) {
149
+ return newErrorResponse({
150
+ code: ErrorCode.TunnelNotFound,
151
+ message: error instanceof Error ? error.message : 'Failed to get tunnel information'
152
+ });
153
+ }
154
+ }
155
+ handleRestart(tunnelid) {
156
+ return __awaiter(this, void 0, void 0, function* () {
157
+ try {
158
+ yield this.tunnelManager.restartTunnel(tunnelid);
159
+ const tunnelConfig = this.tunnelManager.getTunnelConfig("", tunnelid);
160
+ const tunnelInstance = this.tunnelManager.getManagedTunnel("", tunnelid);
161
+ if (!tunnelInstance) {
162
+ throw new Error(`Tunnel instance for ID "${tunnelid}" not found`);
163
+ }
164
+ const stats = this.tunnelManager.getTunnelStats(tunnelid) ? this.tunnelManager.getTunnelStats(tunnelid) : tunnelInstance.instance.getLatestUsage();
165
+ const tunnelResponse = {
166
+ tunnelid: tunnelid,
167
+ remoteurls: this.tunnelManager.getTunnelUrls(tunnelid),
168
+ tunnelconfig: pinggyOptionsToTunnelConfig(tunnelConfig, tunnelInstance.configid, tunnelInstance.tunnelName, this.tunnelManager.getLocalserverTlsInfo(tunnelid), this.tunnelManager.getTunnelGreetMessage(tunnelid)),
169
+ status: newStatus(this.tunnelManager.getTunnelStatus(tunnelid), TunnelErrorCodeType.NoError, ""),
170
+ stats: stats
171
+ };
172
+ return tunnelResponse;
173
+ }
174
+ catch (error) {
175
+ return newErrorResponse({
176
+ code: ErrorCode.TunnelNotFound,
177
+ message: error instanceof Error ? error.message : 'Failed to restart tunnel'
178
+ });
179
+ }
180
+ });
181
+ }
182
+ }
@@ -0,0 +1,159 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import WebSocket from "ws";
11
+ import { logger } from "../logger.js";
12
+ import { handleConnectionStatusMessage, WebSocketCommandHandler } from "./websocket_handlers.js";
13
+ import CLIPrinter from "../utils/printer.js";
14
+ const RECONNECT_SLEEP_MS = 5000; // 5 seconds
15
+ const PING_INTERVAL_MS = 30000; // 30 seconds
16
+ export function buildRemoteManagementWsUrl(manage) {
17
+ let baseUrl = (manage || "dashboard.pinggy.io").trim();
18
+ if (!(baseUrl.startsWith("ws://") || baseUrl.startsWith("wss://"))) {
19
+ baseUrl = "wss://" + baseUrl;
20
+ }
21
+ // Avoid duplicate slashes when concatenating
22
+ const trimmed = baseUrl.replace(/\/$/, "");
23
+ return `${trimmed}/backend/api/v1/remote-management/connect`;
24
+ }
25
+ function extractHostname(u) {
26
+ try {
27
+ const url = new URL(u);
28
+ return url.host;
29
+ }
30
+ catch (_a) {
31
+ return u;
32
+ }
33
+ }
34
+ function sleep(ms) {
35
+ return new Promise((res) => setTimeout(res, ms));
36
+ }
37
+ export function parseRemoteManagement(values) {
38
+ return __awaiter(this, void 0, void 0, function* () {
39
+ const rmToken = values["remote-management"];
40
+ if (typeof rmToken === "string" && rmToken.trim().length > 0) {
41
+ const manageHost = values["manage"];
42
+ try {
43
+ yield initiateRemoteManagement(rmToken, manageHost);
44
+ return { ok: true };
45
+ }
46
+ catch (e) {
47
+ logger.error("Failed to initiate remote management:", e);
48
+ return { ok: false, error: e };
49
+ }
50
+ }
51
+ });
52
+ }
53
+ /**
54
+ * Initiate remote management mode with a WebSocket connection.
55
+ * - Connect with Authorization: Bearer <token>
56
+ * - On HTTP 401: print Unauthorized and exit
57
+ * - On other failures: retry every 15 seconds
58
+ * - Keep running until closed or SIGINT
59
+ */
60
+ export function initiateRemoteManagement(token, manage) {
61
+ return __awaiter(this, void 0, void 0, function* () {
62
+ if (!token || token.trim().length === 0) {
63
+ throw new Error("Remote management token is required (use --remote-management <TOKEN>)");
64
+ }
65
+ const wsUrl = buildRemoteManagementWsUrl(manage);
66
+ const wsHost = extractHostname(wsUrl);
67
+ logger.info("Remote management mode enabled.");
68
+ // Ensure process exits cleanly on Ctrl+C
69
+ let stopRequested = false;
70
+ const sigintHandler = () => { stopRequested = true; };
71
+ process.once('SIGINT', sigintHandler);
72
+ let firstTry = true;
73
+ while (!stopRequested) {
74
+ if (firstTry) {
75
+ firstTry = false;
76
+ CLIPrinter.print(`Connecting to ${wsHost}`);
77
+ logger.info("Connecting to remote management", { wsUrl });
78
+ }
79
+ else {
80
+ CLIPrinter.warn(`Reconnecting in ${RECONNECT_SLEEP_MS / 1000} seconds.`);
81
+ logger.info("Reconnecting after sleep", { seconds: RECONNECT_SLEEP_MS / 1000 });
82
+ yield sleep(RECONNECT_SLEEP_MS);
83
+ if (stopRequested)
84
+ break;
85
+ CLIPrinter.print(`Connecting to ${wsHost}`);
86
+ logger.info("Connecting to remote management", { wsUrl });
87
+ }
88
+ const ws = new WebSocket(wsUrl, {
89
+ headers: { Authorization: `Bearer ${token}` },
90
+ });
91
+ yield new Promise((resolve) => {
92
+ let heartbeat;
93
+ const startHeartbeat = () => {
94
+ heartbeat = setInterval(() => {
95
+ if (ws.readyState !== WebSocket.OPEN)
96
+ return;
97
+ ws.ping(); // ask server for pong
98
+ }, PING_INTERVAL_MS);
99
+ };
100
+ ws.on("open", () => {
101
+ CLIPrinter.success(`Connected to ${wsHost}`);
102
+ startHeartbeat();
103
+ });
104
+ // Respond to server pings
105
+ ws.on("ping", () => ws.pong());
106
+ let firstMessage = true;
107
+ ws.on('message', (data) => __awaiter(this, void 0, void 0, function* () {
108
+ try {
109
+ if (firstMessage) {
110
+ firstMessage = false;
111
+ const ok = handleConnectionStatusMessage(data);
112
+ if (!ok) {
113
+ // The status message itself indicates failure, so close and retry.
114
+ ws.close();
115
+ }
116
+ return; // Wait for the next message (commands)
117
+ }
118
+ const text = data.toString('utf8');
119
+ const req = JSON.parse(text);
120
+ const webSocketHandler = new WebSocketCommandHandler();
121
+ yield webSocketHandler.handle(ws, req);
122
+ }
123
+ catch (e) {
124
+ logger.warn("Failed handling websocket message", { error: String(e) });
125
+ }
126
+ }));
127
+ ws.on('unexpected-response', (_req, res) => {
128
+ if (res.statusCode === 401) {
129
+ CLIPrinter.error("Unauthorized. Please enter a valid token.");
130
+ logger.error("Unauthorized (401) on remote management connect");
131
+ stopRequested = true; // Mark to stop retrying
132
+ ws.close(); // This will trigger 'close' and resolve the promise
133
+ }
134
+ else {
135
+ CLIPrinter.warn(`Unexpected HTTP response ${res.statusCode} from server. Retrying...`);
136
+ logger.warn("Unexpected HTTP response on WebSocket connect", { statusCode: res.statusCode });
137
+ ws.close(); // Trigger 'close' to retry
138
+ }
139
+ });
140
+ ws.on('close', (code, reason) => {
141
+ logger.info("WebSocket closed", { code, reason: reason.toString() });
142
+ CLIPrinter.warn(`Disconnected from remote management (code: ${code}).Retrying...`);
143
+ clearInterval(heartbeat);
144
+ resolve(); // End the promise, allowing the while loop to continue
145
+ });
146
+ ws.on('error', (err) => {
147
+ CLIPrinter.error(err);
148
+ logger.warn("WebSocket error", { error: err.message });
149
+ clearInterval(heartbeat);
150
+ resolve(); // End the promise, allowing the while loop to continue
151
+ });
152
+ });
153
+ if (stopRequested)
154
+ break;
155
+ }
156
+ process.removeListener('SIGINT', sigintHandler);
157
+ logger.info("Remote management stopped.");
158
+ });
159
+ }
@@ -0,0 +1,122 @@
1
+ import { z } from "zod";
2
+ export const HeaderModificationSchema = z.object({
3
+ key: z.string(),
4
+ value: z.array(z.string()).optional(),
5
+ type: z.enum(["add", "remove", "update"]),
6
+ });
7
+ // TunnelConfig schema
8
+ export const TunnelConfigSchema = z.object({
9
+ allowpreflight: z.boolean(),
10
+ autoreconnect: z.boolean(),
11
+ basicauth: z.array(z.object({ username: z.string(), password: z.string() })).nullable(),
12
+ bearerauth: z.string().nullable(),
13
+ configid: z.string().uuid(),
14
+ configname: z.string(),
15
+ greetmsg: z.string().optional(),
16
+ force: z.boolean(),
17
+ forwardedhost: z.string(),
18
+ fullRequestUrl: z.boolean(),
19
+ headermodification: z.array(HeaderModificationSchema),
20
+ httpsOnly: z.boolean(),
21
+ internalwebdebuggerport: z.number(),
22
+ ipwhitelist: z.array(z.string()).nullable(),
23
+ localport: z.number(),
24
+ localsservertls: z.union([z.boolean(), z.string()]),
25
+ localservertlssni: z.string().nullable(),
26
+ regioncode: z.string(),
27
+ noReverseProxy: z.boolean(),
28
+ serveraddress: z.string(),
29
+ serverport: z.number(),
30
+ statusCheckInterval: z.number(),
31
+ token: z.string(),
32
+ tunnelTimeout: z.number(),
33
+ type: z.enum(["http" /* TunnelType.Http */, "tcp" /* TunnelType.Tcp */, "udp" /* TunnelType.Udp */, "tls" /* TunnelType.Tls */, "tlstcp" /* TunnelType.TlsTcp */]),
34
+ webdebuggerport: z.number(),
35
+ xff: z.string(),
36
+ });
37
+ /**
38
+ * Schema for the payload used to manage tunnels using websocket.
39
+ *
40
+ * @remarks
41
+ * This schema is intended for input validation (e.g. API request bodies or remote management socket data)
42
+ * and enforces structural and primitive constraints but does not
43
+ * perform side effects.
44
+ */
45
+ export const StartSchema = z.object({
46
+ tunnelID: z.string().uuid().nullable().optional(),
47
+ tunnelConfig: TunnelConfigSchema,
48
+ });
49
+ export const StopSchema = z.object({
50
+ tunnelID: z.string().min(1),
51
+ });
52
+ export const GetSchema = StopSchema;
53
+ export const RestartSchema = StopSchema;
54
+ export const UpdateConfigSchema = z.object({
55
+ tunnelConfig: TunnelConfigSchema,
56
+ });
57
+ export function tunnelConfigToPinggyOptions(config) {
58
+ return {
59
+ token: config.token || "",
60
+ serverAddress: config.serveraddress || "free.pinggy.io",
61
+ forwarding: `${config.forwardedhost || "localhost"}:${config.localport}`,
62
+ webDebugger: config.webdebuggerport ? `localhost:${config.webdebuggerport}` : "",
63
+ tunnelType: Array.isArray(config.type) ? config.type : [config.type || "http" /* TunnelType.Http */],
64
+ ipWhitelist: config.ipwhitelist || [],
65
+ basicAuth: config.basicauth ? config.basicauth : [],
66
+ bearerTokenAuth: config.bearerauth ? [config.bearerauth] : [],
67
+ headerModification: config.headermodification,
68
+ xForwardedFor: !!config.xff,
69
+ httpsOnly: config.httpsOnly,
70
+ originalRequestUrl: config.fullRequestUrl,
71
+ allowPreflight: config.allowpreflight,
72
+ reverseProxy: config.noReverseProxy,
73
+ force: config.force,
74
+ optional: {
75
+ sniServerName: config.localservertlssni || "",
76
+ },
77
+ };
78
+ }
79
+ export function pinggyOptionsToTunnelConfig(opts, configid, configName, localserverTls, greetMsg) {
80
+ var _a, _b, _c, _d, _e, _f, _g;
81
+ const forwarding = Array.isArray(opts.forwarding) ? String(opts.forwarding[0].address).replace("//", "").replace(/\/$/, "") : String(opts.forwarding).replace("//", "").replace(/\/$/, "");
82
+ const tunnelType = Array.isArray(opts.tunnelType)
83
+ ? opts.tunnelType[0]
84
+ : ((_a = opts.tunnelType) !== null && _a !== void 0 ? _a : "http");
85
+ const parsedTokens = opts.bearerTokenAuth ? (Array.isArray(opts.bearerTokenAuth)
86
+ ? opts.bearerTokenAuth : JSON.parse(opts.bearerTokenAuth)) : [];
87
+ return {
88
+ allowpreflight: (_b = opts.allowPreflight) !== null && _b !== void 0 ? _b : false,
89
+ autoreconnect: true,
90
+ basicauth: opts.basicAuth && Object.keys(opts.basicAuth).length
91
+ ? opts.basicAuth
92
+ : null,
93
+ bearerauth: parsedTokens.length ? parsedTokens.join(',') : null,
94
+ configid: configid,
95
+ configname: configName,
96
+ greetmsg: greetMsg || "",
97
+ force: (_c = opts.force) !== null && _c !== void 0 ? _c : false,
98
+ forwardedhost: (forwarding === null || forwarding === void 0 ? void 0 : forwarding.split(":")[1]) || "localhost",
99
+ fullRequestUrl: (_d = opts.originalRequestUrl) !== null && _d !== void 0 ? _d : false,
100
+ headermodification: opts.headerModification || [], //structured list
101
+ httpsOnly: (_e = opts.httpsOnly) !== null && _e !== void 0 ? _e : false,
102
+ internalwebdebuggerport: 0,
103
+ ipwhitelist: opts.ipWhitelist
104
+ ? (Array.isArray(opts.ipWhitelist)
105
+ ? opts.ipWhitelist
106
+ : JSON.parse(opts.ipWhitelist))
107
+ : null,
108
+ localport: parseInt((forwarding === null || forwarding === void 0 ? void 0 : forwarding.split(":")[2]) || "0", 10),
109
+ localservertlssni: null,
110
+ regioncode: "",
111
+ noReverseProxy: (_f = opts.reverseProxy) !== null && _f !== void 0 ? _f : false,
112
+ serveraddress: opts.serverAddress || "free.pinggy.io",
113
+ serverport: 0,
114
+ statusCheckInterval: 0,
115
+ token: opts.token || "",
116
+ tunnelTimeout: 0,
117
+ type: tunnelType,
118
+ webdebuggerport: Number((_g = opts.webDebugger) === null || _g === void 0 ? void 0 : _g.split(":")[0]) || 0,
119
+ xff: opts.xForwardedFor ? "1" : "",
120
+ localsservertls: localserverTls || false
121
+ };
122
+ }
@@ -0,0 +1,163 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { logger } from "../logger.js";
11
+ import { ErrorCode, NewErrorResponseObject, isErrorResponse, NewResponseObject } from "../types.js";
12
+ import { TunnelOperations } from "./handler.js";
13
+ import { GetSchema, RestartSchema, StartSchema, StopSchema, UpdateConfigSchema } from "./remote_schema.js";
14
+ import z from "zod";
15
+ import CLIPrinter from "../utils/printer.js";
16
+ export class WebSocketCommandHandler {
17
+ constructor() {
18
+ this.tunnelHandler = new TunnelOperations();
19
+ }
20
+ safeParse(text) {
21
+ if (!text)
22
+ return undefined;
23
+ try {
24
+ return JSON.parse(text);
25
+ }
26
+ catch (e) {
27
+ logger.warn("Invalid JSON payload", { error: String(e), text });
28
+ return undefined;
29
+ }
30
+ }
31
+ sendResponse(ws, resp) {
32
+ const payload = Object.assign(Object.assign({}, resp), { response: Buffer.from(resp.response || []).toString("base64") });
33
+ ws.send(JSON.stringify(payload));
34
+ }
35
+ sendError(ws, req, message, code = ErrorCode.InternalServerError) {
36
+ const resp = NewErrorResponseObject({ code, message });
37
+ resp.command = req.command || "";
38
+ resp.requestid = req.requestid || "";
39
+ this.sendResponse(ws, resp);
40
+ }
41
+ handleStartReq(req, raw) {
42
+ return __awaiter(this, void 0, void 0, function* () {
43
+ const dc = StartSchema.parse(raw);
44
+ const result = yield this.tunnelHandler.handleStart(dc.tunnelConfig);
45
+ return this.wrapResponse(result, req);
46
+ });
47
+ }
48
+ handleStopReq(req, raw) {
49
+ return __awaiter(this, void 0, void 0, function* () {
50
+ const dc = StopSchema.parse(raw);
51
+ const result = yield this.tunnelHandler.handleStop(dc.tunnelID);
52
+ return this.wrapResponse(result, req);
53
+ });
54
+ }
55
+ handleGetReq(req, raw) {
56
+ return __awaiter(this, void 0, void 0, function* () {
57
+ const dc = GetSchema.parse(raw);
58
+ const result = yield this.tunnelHandler.handleGet(dc.tunnelID);
59
+ return this.wrapResponse(result, req);
60
+ });
61
+ }
62
+ handleRestartReq(req, raw) {
63
+ return __awaiter(this, void 0, void 0, function* () {
64
+ const dc = RestartSchema.parse(raw);
65
+ const result = yield this.tunnelHandler.handleRestart(dc.tunnelID);
66
+ return this.wrapResponse(result, req);
67
+ });
68
+ }
69
+ handleUpdateConfigReq(req, raw) {
70
+ return __awaiter(this, void 0, void 0, function* () {
71
+ const dc = UpdateConfigSchema.parse(raw);
72
+ const result = yield this.tunnelHandler.handleUpdateConfig(dc.tunnelConfig);
73
+ return this.wrapResponse(result, req);
74
+ });
75
+ }
76
+ handleListReq(req) {
77
+ return __awaiter(this, void 0, void 0, function* () {
78
+ const result = yield this.tunnelHandler.handleList();
79
+ return this.wrapResponse(result, req);
80
+ });
81
+ }
82
+ wrapResponse(result, req) {
83
+ if (isErrorResponse(result)) {
84
+ const errResp = NewErrorResponseObject(result);
85
+ errResp.command = req.command;
86
+ errResp.requestid = req.requestid;
87
+ return errResp;
88
+ }
89
+ const respObj = NewResponseObject(result);
90
+ respObj.command = req.command;
91
+ respObj.requestid = req.requestid;
92
+ return respObj;
93
+ }
94
+ handle(ws, req) {
95
+ return __awaiter(this, void 0, void 0, function* () {
96
+ const cmd = (req.command || "").toLowerCase();
97
+ const raw = this.safeParse(req.data);
98
+ try {
99
+ let response;
100
+ switch (cmd) {
101
+ case "start": {
102
+ response = yield this.handleStartReq(req, raw);
103
+ break;
104
+ }
105
+ case "stop": {
106
+ response = yield this.handleStopReq(req, raw);
107
+ break;
108
+ }
109
+ case "get": {
110
+ response = yield this.handleGetReq(req, raw);
111
+ break;
112
+ }
113
+ case "restart": {
114
+ response = yield this.handleRestartReq(req, raw);
115
+ break;
116
+ }
117
+ case "updateconfig": {
118
+ response = yield this.handleUpdateConfigReq(req, raw);
119
+ break;
120
+ }
121
+ case "list": {
122
+ response = yield this.handleListReq(req);
123
+ break;
124
+ }
125
+ default:
126
+ if (typeof req.command === 'string') {
127
+ logger.warn("Unknown command", { command: req.command });
128
+ }
129
+ return this.sendError(ws, req, "Invalid command");
130
+ }
131
+ logger.debug("Sending response", { command: response.command, requestid: response.requestid });
132
+ this.sendResponse(ws, response);
133
+ }
134
+ catch (e) {
135
+ if (e instanceof z.ZodError) {
136
+ logger.warn("Validation failed", { cmd, issues: e.issues });
137
+ return this.sendError(ws, req, "Invalid request data", ErrorCode.InvalidBodyFormatError);
138
+ }
139
+ logger.error("Error handling command", { cmd, error: String(e) });
140
+ return this.sendError(ws, req, (e === null || e === void 0 ? void 0 : e.message) || "Internal error");
141
+ }
142
+ });
143
+ }
144
+ }
145
+ // // Returns true if connection status is OK else sends logs and returns false
146
+ export function handleConnectionStatusMessage(firstMessage) {
147
+ try {
148
+ const text = typeof firstMessage === 'string' ? firstMessage : firstMessage.toString();
149
+ const cs = JSON.parse(text);
150
+ if (!cs.success) {
151
+ const msg = cs.error_msg || "Connection failed";
152
+ CLIPrinter.warn(`Connection failed: ${msg}`);
153
+ logger.warn("Remote management connection failed", { error_code: cs.error_code, error_msg: msg });
154
+ return false;
155
+ }
156
+ return true;
157
+ }
158
+ catch (e) {
159
+ logger.warn("Failed to parse connection status message", { error: String(e) });
160
+ // If parsing fails, assume connection is okay
161
+ return true;
162
+ }
163
+ }
@@ -0,0 +1,7 @@
1
+ export const asciiArtPinggyLogo = `
2
+ ██████╗ ██╗███╗ ██╗ ██████╗ ██████╗██╗ ██╗
3
+ ██╔══██╗██║████╗ ██║██╔════╝ ██╔════╝╚██╗ ██╔╝
4
+ ██████╔╝██║██╔██╗ ██║██║ ███╗██║ ███╗╚████╔╝
5
+ ██╔═══╝ ██║██║╚██╗██║██║ ██║██║ ██║ ╚██╔╝
6
+ ██║ ██║██║ ╚████║╚██████╔╝╚██████╔╝ ██║
7
+ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚═╝ `;