pinggy 0.3.4 → 0.3.6
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/README.md +1 -1
- package/dist/chunk-65R2GMKQ.js +2101 -0
- package/dist/index.cjs +1814 -1362
- package/dist/index.d.cts +616 -0
- package/dist/index.d.ts +616 -0
- package/dist/index.js +38 -55
- package/dist/{main-CZY6GID4.js → main-2QDG7PWL.js} +229 -1726
- package/package.json +3 -4
- package/.github/workflows/npm-publish-github-packages.yml +0 -34
- package/.github/workflows/publish-binaries.yml +0 -223
- package/Makefile +0 -4
- package/caxa_build.js +0 -24
- package/dist/chunk-T5ESYDJY.js +0 -121
- package/ent.plist +0 -14
- package/jest.config.js +0 -19
- package/src/_tests_/build_config.test.ts +0 -91
- package/src/cli/buildConfig.ts +0 -475
- package/src/cli/defaults.ts +0 -20
- package/src/cli/extendedOptions.ts +0 -153
- package/src/cli/help.ts +0 -43
- package/src/cli/options.ts +0 -50
- package/src/cli/starCli.ts +0 -229
- package/src/index.ts +0 -30
- package/src/logger.ts +0 -138
- package/src/main.ts +0 -87
- package/src/remote_management/handler.ts +0 -244
- package/src/remote_management/remoteManagement.ts +0 -226
- package/src/remote_management/remote_schema.ts +0 -176
- package/src/remote_management/websocket_handlers.ts +0 -180
- package/src/tui/blessed/TunnelTui.ts +0 -340
- package/src/tui/blessed/components/DisplayUpdaters.ts +0 -189
- package/src/tui/blessed/components/KeyBindings.ts +0 -236
- package/src/tui/blessed/components/Modals.ts +0 -302
- package/src/tui/blessed/components/UIComponents.ts +0 -306
- package/src/tui/blessed/components/index.ts +0 -4
- package/src/tui/blessed/config.ts +0 -53
- package/src/tui/blessed/headerFetcher.ts +0 -42
- package/src/tui/blessed/index.ts +0 -2
- package/src/tui/blessed/qrCodeGenerator.ts +0 -20
- package/src/tui/blessed/webDebuggerConnection.ts +0 -128
- package/src/tui/ink/asciArt.ts +0 -7
- package/src/tui/ink/hooks/useQrCodes.ts +0 -27
- package/src/tui/ink/hooks/useReqResHeaders.ts +0 -27
- package/src/tui/ink/hooks/useTerminalSize.ts +0 -26
- package/src/tui/ink/hooks/useTerminalStats.ts +0 -24
- package/src/tui/ink/hooks/useWebDebugger.ts +0 -98
- package/src/tui/ink/index.tsx +0 -243
- package/src/tui/ink/layout/Borders.tsx +0 -15
- package/src/tui/ink/layout/Container.tsx +0 -15
- package/src/tui/ink/sections/DebuggerDetailModal.tsx +0 -53
- package/src/tui/ink/sections/KeyBindings.tsx +0 -58
- package/src/tui/ink/sections/QrCodeSection.tsx +0 -28
- package/src/tui/ink/sections/StatsSection.tsx +0 -20
- package/src/tui/ink/sections/URLsSection.tsx +0 -53
- package/src/tui/ink/utils/utils.ts +0 -35
- package/src/tui/spinner/spinner.ts +0 -64
- package/src/tunnel_manager/TunnelManager.ts +0 -1212
- package/src/types.ts +0 -255
- package/src/utils/FileServer.ts +0 -112
- package/src/utils/detect_vc_redist_on_windows.ts +0 -167
- package/src/utils/getFreePort.ts +0 -41
- package/src/utils/htmlTemplates.ts +0 -146
- package/src/utils/parseArgs.ts +0 -79
- package/src/utils/printer.ts +0 -81
- package/src/utils/util.ts +0 -18
- package/src/workers/file_serve_worker.ts +0 -33
- package/tsconfig.json +0 -17
- package/tsup.config.ts +0 -12
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ErrorCode,
|
|
3
|
-
Status,
|
|
4
|
-
newErrorResponse,
|
|
5
|
-
ErrorResponse,
|
|
6
|
-
newStatus,
|
|
7
|
-
TunnelStateType,
|
|
8
|
-
TunnelErrorCodeType,
|
|
9
|
-
newStats,
|
|
10
|
-
ErrorCodeType,
|
|
11
|
-
AdditionalForwarding
|
|
12
|
-
} from "../types.js";
|
|
13
|
-
import { DisconnectListener, TunnelManager } from "../tunnel_manager/TunnelManager.js";
|
|
14
|
-
import { pinggyOptionsToTunnelConfig, tunnelConfigToPinggyOptions, TunnelConfig } from "./remote_schema.js";
|
|
15
|
-
import { PinggyOptions, TunnelType, TunnelUsageType } from "@pinggy/pinggy";
|
|
16
|
-
|
|
17
|
-
export interface TunnelResponse {
|
|
18
|
-
tunnelid: string;
|
|
19
|
-
remoteurls: string[];
|
|
20
|
-
tunnelconfig: TunnelConfig;
|
|
21
|
-
status: Status;
|
|
22
|
-
stats: TunnelUsageType;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
interface TunnelHandler {
|
|
26
|
-
handleStart(config: TunnelConfig): Promise<TunnelResponse | ErrorResponse>;
|
|
27
|
-
handleUpdateConfig(config: TunnelConfig): Promise<TunnelResponse | ErrorResponse>;
|
|
28
|
-
handleList(): Promise<TunnelResponse[] | ErrorResponse>;
|
|
29
|
-
handleStop(tunnelid: string): Promise<TunnelResponse | ErrorResponse>;
|
|
30
|
-
handleGet(tunnelid: string): Promise<TunnelResponse | ErrorResponse>;
|
|
31
|
-
handleRestart(tunnelid: string): Promise<TunnelResponse | ErrorResponse>;
|
|
32
|
-
handleRegisterStatsListener(tunnelid: string, listener: (tunnelId: string, stats: TunnelUsageType) => void): void;
|
|
33
|
-
handleUnregisterStatsListener(tunnelid: string, listnerId: string): void;
|
|
34
|
-
handleGetTunnelStats(tunnelid: string): TunnelUsageType[] | ErrorResponse;
|
|
35
|
-
handleRegisterDisconnectListener(tunnelid: string, listener: DisconnectListener): void;
|
|
36
|
-
handleRemoveStoppedTunnelByTunnelId(tunnelId: string): boolean | ErrorResponse;
|
|
37
|
-
handleRemoveStoppedTunnelByConfigId(configId: string): boolean | ErrorResponse;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export class TunnelOperations implements TunnelHandler {
|
|
41
|
-
private tunnelManager: TunnelManager;
|
|
42
|
-
|
|
43
|
-
constructor() {
|
|
44
|
-
this.tunnelManager = TunnelManager.getInstance(); // Use singleton instance
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
private buildStatus(tunnelId: string, state: TunnelStateType, errorCode: TunnelErrorCodeType): Status {
|
|
49
|
-
const status = newStatus(state, errorCode, "");
|
|
50
|
-
try {
|
|
51
|
-
const managed = this.tunnelManager.getManagedTunnel("", tunnelId);
|
|
52
|
-
if (managed) {
|
|
53
|
-
status.createdtimestamp = managed.createdAt || "";
|
|
54
|
-
status.starttimestamp = managed.startedAt || "";
|
|
55
|
-
status.endtimestamp = managed.stoppedAt || "";
|
|
56
|
-
}
|
|
57
|
-
} catch (e) {
|
|
58
|
-
//ignore
|
|
59
|
-
}
|
|
60
|
-
return status;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// --- Helper to construct TunnelResponse ---
|
|
64
|
-
private async buildTunnelResponse(tunnelid: string, tunnelConfig: PinggyOptions, configid: string, tunnelName: string, additionalForwarding?: AdditionalForwarding[], serve?: string): Promise<TunnelResponse> {
|
|
65
|
-
const [status, stats, tlsInfo, greetMsg, remoteurls] = await Promise.all([
|
|
66
|
-
this.tunnelManager.getTunnelStatus(tunnelid),
|
|
67
|
-
this.tunnelManager.getLatestTunnelStats(tunnelid) || newStats(),
|
|
68
|
-
this.tunnelManager.getLocalserverTlsInfo(tunnelid),
|
|
69
|
-
this.tunnelManager.getTunnelGreetMessage(tunnelid),
|
|
70
|
-
this.tunnelManager.getTunnelUrls(tunnelid)
|
|
71
|
-
]);
|
|
72
|
-
|
|
73
|
-
return {
|
|
74
|
-
tunnelid,
|
|
75
|
-
remoteurls,
|
|
76
|
-
tunnelconfig: pinggyOptionsToTunnelConfig(tunnelConfig, configid, tunnelName, tlsInfo, greetMsg as string, additionalForwarding),
|
|
77
|
-
status: this.buildStatus(tunnelid, status as TunnelStateType, TunnelErrorCodeType.NoError),
|
|
78
|
-
stats
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
private error(code: ErrorCodeType, err: unknown, fallback: string): ErrorResponse {
|
|
83
|
-
return newErrorResponse({
|
|
84
|
-
code,
|
|
85
|
-
message: err instanceof Error ? err.message : fallback
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// --- Operations ---
|
|
90
|
-
async handleStart(config: TunnelConfig): Promise<TunnelResponse | ErrorResponse> {
|
|
91
|
-
|
|
92
|
-
try {
|
|
93
|
-
// Convert TunnelConfig -> PinggyOptions
|
|
94
|
-
const opts = tunnelConfigToPinggyOptions(config);
|
|
95
|
-
const additionalForwardingParsed = config.additionalForwarding || [];
|
|
96
|
-
const { tunnelid, instance, tunnelName, additionalForwarding, serve } = await this.tunnelManager.createTunnel({
|
|
97
|
-
...opts,
|
|
98
|
-
tunnelType: Array.isArray(config.type) ? config.type : (config.type ? [config.type] : [TunnelType.Http]), // Temporary fix in future we will not use this field.
|
|
99
|
-
configid: config.configid,
|
|
100
|
-
tunnelName: config.configname,
|
|
101
|
-
additionalForwarding: additionalForwardingParsed,
|
|
102
|
-
serve: config.serve
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
this.tunnelManager.startTunnel(tunnelid);
|
|
106
|
-
const tunnelPconfig = await this.tunnelManager.getTunnelConfig("", tunnelid);
|
|
107
|
-
const resp = this.buildTunnelResponse(tunnelid, tunnelPconfig, config.configid, tunnelName as string, additionalForwarding, serve);
|
|
108
|
-
return resp;
|
|
109
|
-
} catch (err) {
|
|
110
|
-
return this.error(ErrorCode.ErrorStartingTunnel, err, "Unknown error occurred while starting tunnel");
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
async handleUpdateConfig(config: TunnelConfig): Promise<TunnelResponse | ErrorResponse> {
|
|
115
|
-
try {
|
|
116
|
-
const opts = tunnelConfigToPinggyOptions(config);
|
|
117
|
-
const tunnel = await this.tunnelManager.updateConfig({
|
|
118
|
-
...opts,
|
|
119
|
-
tunnelType: Array.isArray(config.type) ? config.type : (config.type ? [config.type] : [TunnelType.Http]), // // Temporary fix in future we will not use this field.
|
|
120
|
-
configid: config.configid,
|
|
121
|
-
tunnelName: config.configname,
|
|
122
|
-
additionalForwarding: config.additionalForwarding || [],
|
|
123
|
-
serve: config.serve
|
|
124
|
-
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
if (!tunnel.instance || !tunnel.tunnelConfig)
|
|
128
|
-
throw new Error("Invalid tunnel state after configuration update");
|
|
129
|
-
|
|
130
|
-
return this.buildTunnelResponse(tunnel.tunnelid, tunnel.tunnelConfig, config.configid, tunnel.tunnelName as string, tunnel.additionalForwarding, tunnel.serve);
|
|
131
|
-
} catch (err) {
|
|
132
|
-
return this.error(ErrorCode.InternalServerError, err, "Failed to update tunnel configuration");
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
async handleList(): Promise<TunnelResponse[] | ErrorResponse> {
|
|
137
|
-
|
|
138
|
-
try {
|
|
139
|
-
const tunnels = await this.tunnelManager.getAllTunnels();
|
|
140
|
-
if (tunnels.length === 0) {
|
|
141
|
-
return [];
|
|
142
|
-
}
|
|
143
|
-
return Promise.all(
|
|
144
|
-
tunnels.map(async (t) => {
|
|
145
|
-
const rawStats = this.tunnelManager.getLatestTunnelStats(t.tunnelid) || newStats();
|
|
146
|
-
const [status, tlsInfo, greetMsg] = await Promise.all([
|
|
147
|
-
this.tunnelManager.getTunnelStatus(t.tunnelid),
|
|
148
|
-
this.tunnelManager.getLocalserverTlsInfo(t.tunnelid),
|
|
149
|
-
this.tunnelManager.getTunnelGreetMessage(t.tunnelid)
|
|
150
|
-
]);
|
|
151
|
-
const pinggyOptions = status !== TunnelStateType.Closed && status !== TunnelStateType.Exited
|
|
152
|
-
? await this.tunnelManager.getTunnelConfig("", t.tunnelid)
|
|
153
|
-
: t.tunnelConfig!;
|
|
154
|
-
|
|
155
|
-
const tunnelConfig = pinggyOptionsToTunnelConfig(pinggyOptions, t.configid, t.tunnelName as string, tlsInfo, greetMsg, t.additionalForwarding, t.serve);
|
|
156
|
-
|
|
157
|
-
return {
|
|
158
|
-
tunnelid: t.tunnelid,
|
|
159
|
-
remoteurls: t.remoteurls,
|
|
160
|
-
status: this.buildStatus(t.tunnelid, status as TunnelStateType, TunnelErrorCodeType.NoError),
|
|
161
|
-
stats: rawStats,
|
|
162
|
-
tunnelconfig: tunnelConfig
|
|
163
|
-
};
|
|
164
|
-
})
|
|
165
|
-
);
|
|
166
|
-
} catch (err) {
|
|
167
|
-
return this.error(ErrorCode.InternalServerError, err, "Failed to list tunnels");
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
async handleStop(tunnelid: string): Promise<TunnelResponse | ErrorResponse> {
|
|
172
|
-
try {
|
|
173
|
-
const { configid } = this.tunnelManager.stopTunnel(tunnelid);
|
|
174
|
-
const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
|
|
175
|
-
if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
|
|
176
|
-
return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, configid, managed.tunnelName as string, managed.additionalForwarding, managed.serve);
|
|
177
|
-
} catch (err) {
|
|
178
|
-
return this.error(ErrorCode.TunnelNotFound, err, "Failed to stop tunnel");
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
async handleGet(tunnelid: string): Promise<TunnelResponse | ErrorResponse> {
|
|
183
|
-
try {
|
|
184
|
-
const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
|
|
185
|
-
if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
|
|
186
|
-
return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, managed.configid, managed.tunnelName as string, managed.additionalForwarding, managed.serve);
|
|
187
|
-
} catch (err) {
|
|
188
|
-
return this.error(ErrorCode.TunnelNotFound, err, "Failed to get tunnel information");
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
async handleRestart(tunnelid: string): Promise<TunnelResponse | ErrorResponse> {
|
|
193
|
-
try {
|
|
194
|
-
await this.tunnelManager.restartTunnel(tunnelid);
|
|
195
|
-
const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
|
|
196
|
-
if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
|
|
197
|
-
return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, managed.configid, managed.tunnelName as string, managed.additionalForwarding, managed.serve);
|
|
198
|
-
} catch (err) {
|
|
199
|
-
return this.error(ErrorCode.TunnelNotFound, err, "Failed to restart tunnel");
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
handleRegisterStatsListener(tunnelid: string, listener: (tunnelId: string, stats: TunnelUsageType) => void): void {
|
|
203
|
-
this.tunnelManager.registerStatsListener(tunnelid, listener);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
handleUnregisterStatsListener(tunnelid: string, listnerId: string): void {
|
|
207
|
-
this.tunnelManager.deregisterStatsListener(tunnelid, listnerId);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
handleGetTunnelStats(tunnelid: string): TunnelUsageType[] | ErrorResponse {
|
|
211
|
-
try {
|
|
212
|
-
const stats = this.tunnelManager.getTunnelStats(tunnelid);
|
|
213
|
-
if (!stats) {
|
|
214
|
-
// if no stats found, return new stats object
|
|
215
|
-
return [newStats()];
|
|
216
|
-
}
|
|
217
|
-
return stats;
|
|
218
|
-
} catch (err) {
|
|
219
|
-
return this.error(ErrorCode.TunnelNotFound, err, "Failed to get tunnel stats");
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
handleRegisterDisconnectListener(tunnelid: string, listener: DisconnectListener): void {
|
|
224
|
-
this.tunnelManager.registerDisconnectListener(tunnelid, listener);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
handleRemoveStoppedTunnelByConfigId(configId: string): boolean | ErrorResponse {
|
|
228
|
-
try {
|
|
229
|
-
return this.tunnelManager.removeStoppedTunnelByConfigId(configId);
|
|
230
|
-
} catch (err) {
|
|
231
|
-
|
|
232
|
-
return this.error(ErrorCode.InternalServerError, err, "Failed to remove stopped tunnel by configId");
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
handleRemoveStoppedTunnelByTunnelId(tunnelId: string): boolean | ErrorResponse {
|
|
237
|
-
try {
|
|
238
|
-
return this.tunnelManager.removeStoppedTunnelByTunnelId(tunnelId);
|
|
239
|
-
} catch (err) {
|
|
240
|
-
|
|
241
|
-
return this.error(ErrorCode.InternalServerError, err, "Failed to remove stopped tunnel by tunnelId");
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
import WebSocket from "ws";
|
|
2
|
-
import { logger } from "../logger.js";
|
|
3
|
-
import { handleConnectionStatusMessage, WebSocketCommandHandler, WebSocketRequest } from "./websocket_handlers.js";
|
|
4
|
-
import CLIPrinter from "../utils/printer.js";
|
|
5
|
-
import { RemoteManagementState, RemoteManagementStatus } from "../types.js";
|
|
6
|
-
|
|
7
|
-
const RECONNECT_SLEEP_MS = 5000; // 5 seconds
|
|
8
|
-
const PING_INTERVAL_MS = 30000; // 30 seconds
|
|
9
|
-
|
|
10
|
-
type RemoteManagementResult =
|
|
11
|
-
| { ok: true }
|
|
12
|
-
| { ok: false; error: unknown };
|
|
13
|
-
|
|
14
|
-
interface RemoteManagementValues {
|
|
15
|
-
"remote-management"?: string;
|
|
16
|
-
"manage"?: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
let _remoteManagementState: RemoteManagementState = {
|
|
20
|
-
status: "NOT_RUNNING",
|
|
21
|
-
errorMessage: "",
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
let _stopRequested = false;
|
|
25
|
-
let currentWs: WebSocket | null = null;
|
|
26
|
-
|
|
27
|
-
export function buildRemoteManagementWsUrl(manage?: string): string {
|
|
28
|
-
let baseUrl = (manage || "dashboard.pinggy.io").trim();
|
|
29
|
-
if (!(baseUrl.startsWith("ws://") || baseUrl.startsWith("wss://"))) {
|
|
30
|
-
baseUrl = "wss://" + baseUrl;
|
|
31
|
-
}
|
|
32
|
-
// Avoid duplicate slashes when concatenating
|
|
33
|
-
const trimmed = baseUrl.replace(/\/$/, "");
|
|
34
|
-
return `${trimmed}/backend/api/v1/remote-management/connect`;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function extractHostname(u: string): string {
|
|
38
|
-
try {
|
|
39
|
-
const url = new URL(u);
|
|
40
|
-
return url.host;
|
|
41
|
-
} catch {
|
|
42
|
-
return u;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function sleep(ms: number) {
|
|
47
|
-
return new Promise((res) => setTimeout(res, ms));
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export async function parseRemoteManagement(values: RemoteManagementValues): Promise<RemoteManagementResult | void> {
|
|
51
|
-
const rmToken = values["remote-management"];
|
|
52
|
-
if (typeof rmToken === "string" && rmToken.trim().length > 0) {
|
|
53
|
-
const manageHost = values["manage"];
|
|
54
|
-
try {
|
|
55
|
-
await initiateRemoteManagement(rmToken, manageHost);
|
|
56
|
-
return { ok: true };
|
|
57
|
-
} catch (e) {
|
|
58
|
-
logger.error("Failed to initiate remote management:", e);
|
|
59
|
-
return { ok: false, error: e };
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Initiate remote management mode with a WebSocket connection.
|
|
66
|
-
* - Connect with Authorization: Bearer <token>
|
|
67
|
-
* - On HTTP 401: print Unauthorized and exit
|
|
68
|
-
* - On other failures: retry every 15 seconds
|
|
69
|
-
* - Keep running until closed or SIGINT
|
|
70
|
-
*/
|
|
71
|
-
export async function initiateRemoteManagement(token: string, manage?: string): Promise<RemoteManagementState> {
|
|
72
|
-
|
|
73
|
-
if (!token || token.trim().length === 0) {
|
|
74
|
-
throw new Error("Remote management token is required (use --remote-management <TOKEN>)");
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const wsUrl = buildRemoteManagementWsUrl(manage);
|
|
78
|
-
const wsHost = extractHostname(wsUrl);
|
|
79
|
-
|
|
80
|
-
logger.info("Remote management mode enabled.");
|
|
81
|
-
|
|
82
|
-
// Ensure process exits cleanly on Ctrl+C
|
|
83
|
-
_stopRequested = false;
|
|
84
|
-
const sigintHandler = () => { _stopRequested = true; };
|
|
85
|
-
process.once('SIGINT', sigintHandler);
|
|
86
|
-
|
|
87
|
-
const logConnecting = () => {
|
|
88
|
-
CLIPrinter.print(`Connecting to ${wsHost}`);
|
|
89
|
-
logger.info("Connecting to remote management", { wsUrl });
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
while (!_stopRequested) {
|
|
94
|
-
logConnecting();
|
|
95
|
-
setRemoteManagementState({ status: RemoteManagementStatus.Connecting, errorMessage: "" });
|
|
96
|
-
try {
|
|
97
|
-
await handleWebSocketConnection(wsUrl, wsHost, token);
|
|
98
|
-
} catch (error) {
|
|
99
|
-
logger.warn("Remote management connection error", { error: String(error) });
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
if (_stopRequested) break;
|
|
103
|
-
CLIPrinter.warn(`Remote management disconnected. Reconnecting in ${RECONNECT_SLEEP_MS / 1000} seconds...`);
|
|
104
|
-
logger.info("Reconnecting to remote management after disconnect");
|
|
105
|
-
await sleep(RECONNECT_SLEEP_MS);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
process.removeListener('SIGINT', sigintHandler);
|
|
110
|
-
logger.info("Remote management stopped.");
|
|
111
|
-
return getRemoteManagementState();
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
async function handleWebSocketConnection(wsUrl: string, wsHost: string, token: string): Promise<void> {
|
|
115
|
-
return new Promise<void>((resolve) => {
|
|
116
|
-
|
|
117
|
-
const ws = new WebSocket(wsUrl, {
|
|
118
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
119
|
-
});
|
|
120
|
-
// Tracking the ws for cleanup
|
|
121
|
-
currentWs = ws;
|
|
122
|
-
|
|
123
|
-
let heartbeat: NodeJS.Timeout | null = null;
|
|
124
|
-
let firstMessage = true;
|
|
125
|
-
|
|
126
|
-
/** Safely cleanup on any exit */
|
|
127
|
-
const cleanup = () => {
|
|
128
|
-
if (heartbeat) clearInterval(heartbeat);
|
|
129
|
-
currentWs = null;
|
|
130
|
-
resolve();
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
ws.once("open", () => {
|
|
134
|
-
CLIPrinter.success(`Connected to ${wsHost}`);
|
|
135
|
-
|
|
136
|
-
heartbeat = setInterval(() => {
|
|
137
|
-
if (ws.readyState === WebSocket.OPEN) ws.ping();
|
|
138
|
-
}, PING_INTERVAL_MS);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
ws.on("ping", () => ws.pong());
|
|
142
|
-
|
|
143
|
-
ws.on("message", async (data) => {
|
|
144
|
-
try {
|
|
145
|
-
if (firstMessage) {
|
|
146
|
-
firstMessage = false;
|
|
147
|
-
const ok = handleConnectionStatusMessage(data);
|
|
148
|
-
if (!ok) ws.close();
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
setRemoteManagementState({ status: RemoteManagementStatus.Running, errorMessage: "" });
|
|
152
|
-
const req = JSON.parse(data.toString("utf8")) as WebSocketRequest;
|
|
153
|
-
await new WebSocketCommandHandler().handle(ws, req);
|
|
154
|
-
} catch (e) {
|
|
155
|
-
logger.warn("Failed handling websocket message", { error: String(e) });
|
|
156
|
-
}
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
ws.on("unexpected-response", (_, res) => {
|
|
160
|
-
setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: `HTTP ${res.statusCode}` });
|
|
161
|
-
if (res.statusCode === 401) {
|
|
162
|
-
CLIPrinter.error("Unauthorized. Please enter a valid token.");
|
|
163
|
-
logger.error("Unauthorized (401) on remote management connect");
|
|
164
|
-
} else {
|
|
165
|
-
CLIPrinter.warn(`Unexpected HTTP ${res.statusCode}. Retrying...`);
|
|
166
|
-
logger.warn("Unexpected HTTP response", { statusCode: res.statusCode });
|
|
167
|
-
}
|
|
168
|
-
ws.close();
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
ws.on("close", (code, reason) => {
|
|
172
|
-
setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: "" });
|
|
173
|
-
logger.info("WebSocket closed", { code, reason: reason.toString() });
|
|
174
|
-
CLIPrinter.warn(`Disconnected (code: ${code}). Retrying...`);
|
|
175
|
-
cleanup();
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
ws.on("error", (err) => {
|
|
179
|
-
setRemoteManagementState({ status: RemoteManagementStatus.Error, errorMessage: err.message });
|
|
180
|
-
logger.warn("WebSocket error", { error: err.message });
|
|
181
|
-
CLIPrinter.error(err);
|
|
182
|
-
cleanup();
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
export async function closeRemoteManagement(timeoutMs = 10000): Promise<RemoteManagementState> {
|
|
189
|
-
_stopRequested = true;
|
|
190
|
-
try {
|
|
191
|
-
if (currentWs) {
|
|
192
|
-
try {
|
|
193
|
-
setRemoteManagementState({ status: RemoteManagementStatus.Disconnecting, errorMessage: "" });
|
|
194
|
-
currentWs.close();
|
|
195
|
-
} catch (e) {
|
|
196
|
-
logger.warn("Error while closing current remote management websocket", { error: String(e) });
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const start = Date.now();
|
|
201
|
-
while (_remoteManagementState.status === "RUNNING") {
|
|
202
|
-
if (Date.now() - start > timeoutMs) {
|
|
203
|
-
logger.warn("Timed out waiting for remote management to stop");
|
|
204
|
-
break;
|
|
205
|
-
}
|
|
206
|
-
await sleep(200);
|
|
207
|
-
}
|
|
208
|
-
} finally {
|
|
209
|
-
// Ensure ws ref cleared
|
|
210
|
-
currentWs = null;
|
|
211
|
-
setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: "" });
|
|
212
|
-
return getRemoteManagementState();
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
export function getRemoteManagementState(): RemoteManagementState {
|
|
218
|
-
return _remoteManagementState;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function setRemoteManagementState(state: RemoteManagementState, errorMessage?: string) {
|
|
222
|
-
_remoteManagementState = {
|
|
223
|
-
status: state.status,
|
|
224
|
-
errorMessage: errorMessage || "",
|
|
225
|
-
};
|
|
226
|
-
}
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import { PinggyOptions, TunnelType } from "@pinggy/pinggy";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import { AdditionalForwarding } from "../types.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export const HeaderModificationSchema = z.object({
|
|
7
|
-
key: z.string(),
|
|
8
|
-
value: z.array(z.string()).optional(),
|
|
9
|
-
type: z.enum(["add", "remove", "update"]),
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
export const AdditionalForwardingSchema = z.object({
|
|
13
|
-
remoteDomain: z.string().optional(),
|
|
14
|
-
remotePort: z.number().optional(),
|
|
15
|
-
localDomain: z.string(),
|
|
16
|
-
localPort: z.number(),
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
// TunnelConfig schema
|
|
21
|
-
export const TunnelConfigSchema = z
|
|
22
|
-
.object({
|
|
23
|
-
allowPreflight: z.boolean().optional(), // primary key
|
|
24
|
-
allowpreflight: z.boolean().optional(), // legacy key
|
|
25
|
-
autoreconnect: z.boolean(),
|
|
26
|
-
basicauth: z.array(z.object({ username: z.string(), password: z.string() })).nullable(),
|
|
27
|
-
bearerauth: z.string().nullable(),
|
|
28
|
-
configid: z.string(),
|
|
29
|
-
configname: z.string(),
|
|
30
|
-
greetmsg: z.string().optional(),
|
|
31
|
-
force: z.boolean(),
|
|
32
|
-
forwardedhost: z.string(),
|
|
33
|
-
fullRequestUrl: z.boolean(),
|
|
34
|
-
headermodification: z.array(HeaderModificationSchema),
|
|
35
|
-
httpsOnly: z.boolean(),
|
|
36
|
-
internalwebdebuggerport: z.number(),
|
|
37
|
-
ipwhitelist: z.array(z.string()).nullable(),
|
|
38
|
-
localport: z.number(),
|
|
39
|
-
localsservertls: z.union([z.boolean(), z.string()]),
|
|
40
|
-
localservertlssni: z.string().nullable(),
|
|
41
|
-
regioncode: z.string(),
|
|
42
|
-
noReverseProxy: z.boolean(),
|
|
43
|
-
serveraddress: z.string(),
|
|
44
|
-
serverport: z.number(),
|
|
45
|
-
statusCheckInterval: z.number(),
|
|
46
|
-
token: z.string(),
|
|
47
|
-
tunnelTimeout: z.number(),
|
|
48
|
-
type: z.enum([
|
|
49
|
-
TunnelType.Http,
|
|
50
|
-
TunnelType.Tcp,
|
|
51
|
-
TunnelType.Udp,
|
|
52
|
-
TunnelType.Tls,
|
|
53
|
-
TunnelType.TlsTcp
|
|
54
|
-
]),
|
|
55
|
-
webdebuggerport: z.number(),
|
|
56
|
-
xff: z.string(),
|
|
57
|
-
additionalForwarding: z.array(AdditionalForwardingSchema).optional(),
|
|
58
|
-
serve: z.string().optional(),
|
|
59
|
-
})
|
|
60
|
-
.superRefine((data, ctx) => {
|
|
61
|
-
if (data.allowPreflight === undefined && data.allowpreflight === undefined) {
|
|
62
|
-
ctx.addIssue({
|
|
63
|
-
code: "custom",
|
|
64
|
-
message: "Either allowPreflight or allowpreflight is required",
|
|
65
|
-
path: ["allowPreflight"],
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
})
|
|
69
|
-
.transform((data) => ({
|
|
70
|
-
...data,
|
|
71
|
-
allowPreflight: data.allowPreflight ?? data.allowpreflight,
|
|
72
|
-
allowpreflight: data.allowPreflight ?? data.allowpreflight,
|
|
73
|
-
}));
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Schema for the payload used to manage tunnels using websocket.
|
|
78
|
-
*
|
|
79
|
-
* @remarks
|
|
80
|
-
* This schema is intended for input validation (e.g. API request bodies or remote management socket data)
|
|
81
|
-
* and enforces structural and primitive constraints but does not
|
|
82
|
-
* perform side effects.
|
|
83
|
-
*/
|
|
84
|
-
|
|
85
|
-
export const StartSchema = z.object({
|
|
86
|
-
tunnelID: z.string().nullable().optional(),
|
|
87
|
-
tunnelConfig: TunnelConfigSchema,
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
export const StopSchema = z.object({
|
|
91
|
-
tunnelID: z.string().min(1),
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
export const GetSchema = StopSchema;
|
|
95
|
-
export const RestartSchema = StopSchema;
|
|
96
|
-
|
|
97
|
-
export const UpdateConfigSchema = z.object({
|
|
98
|
-
tunnelConfig: TunnelConfigSchema,
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
export type TunnelConfig = z.infer<typeof TunnelConfigSchema>;
|
|
102
|
-
|
|
103
|
-
export function tunnelConfigToPinggyOptions(config: TunnelConfig): PinggyOptions {
|
|
104
|
-
return {
|
|
105
|
-
token: config.token || "",
|
|
106
|
-
serverAddress: config.serveraddress || "free.pinggy.io",
|
|
107
|
-
forwarding: `${config.forwardedhost || "localhost"}:${config.localport}`,
|
|
108
|
-
webDebugger: config.webdebuggerport ? `localhost:${config.webdebuggerport}` : "",
|
|
109
|
-
ipWhitelist: config.ipwhitelist || [],
|
|
110
|
-
basicAuth: config.basicauth ? config.basicauth : [],
|
|
111
|
-
bearerTokenAuth: config.bearerauth ? [config.bearerauth] : [],
|
|
112
|
-
headerModification: config.headermodification,
|
|
113
|
-
xForwardedFor: !!config.xff,
|
|
114
|
-
httpsOnly: config.httpsOnly,
|
|
115
|
-
originalRequestUrl: config.fullRequestUrl,
|
|
116
|
-
allowPreflight: config.allowPreflight,
|
|
117
|
-
reverseProxy: config.noReverseProxy,
|
|
118
|
-
force: config.force,
|
|
119
|
-
autoReconnect: config.autoreconnect,
|
|
120
|
-
optional: {
|
|
121
|
-
sniServerName: config.localservertlssni || "",
|
|
122
|
-
},
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export function pinggyOptionsToTunnelConfig(opts: PinggyOptions, configid: string, configName: string, localserverTls?: string | boolean, greetMsg?: string | null, additionalForwarding?: AdditionalForwarding[], serve?: string): TunnelConfig {
|
|
127
|
-
|
|
128
|
-
const forwarding: string = Array.isArray(opts.forwarding) ? String(opts.forwarding[0].address).replace("//", "").replace(/\/$/, "") : String(opts.forwarding).replace("//", "").replace(/\/$/, "");
|
|
129
|
-
const parsedForwardedHost = forwarding.split(":").length == 3 ? forwarding.split(":")[1] : forwarding.split(":")[0];
|
|
130
|
-
const parsedLocalPort = forwarding.split(":").length == 3 ? parseInt(forwarding.split(":")[2], 10) : parseInt(forwarding.split(":")[1], 10);
|
|
131
|
-
|
|
132
|
-
const tunnelType =
|
|
133
|
-
(Array.isArray(opts.forwarding) ? opts.forwarding[0]?.type : undefined) ?? TunnelType.Http;
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const parsedTokens: string[] = opts.bearerTokenAuth ? (Array.isArray(opts.bearerTokenAuth)
|
|
137
|
-
? opts.bearerTokenAuth : (JSON.parse(opts.bearerTokenAuth) as string[])) : [];
|
|
138
|
-
return {
|
|
139
|
-
allowPreflight: opts.allowPreflight ?? false,
|
|
140
|
-
allowpreflight: opts.allowPreflight ?? false,
|
|
141
|
-
autoreconnect: opts.autoReconnect ?? false,
|
|
142
|
-
basicauth: opts.basicAuth && Object.keys(opts.basicAuth).length
|
|
143
|
-
? opts.basicAuth
|
|
144
|
-
: null,
|
|
145
|
-
bearerauth: parsedTokens.length ? parsedTokens.join(',') : null,
|
|
146
|
-
configid: configid,
|
|
147
|
-
configname: configName,
|
|
148
|
-
greetmsg: greetMsg || "",
|
|
149
|
-
force: opts.force ?? false,
|
|
150
|
-
forwardedhost: parsedForwardedHost || "localhost",
|
|
151
|
-
fullRequestUrl: opts.originalRequestUrl ?? false,
|
|
152
|
-
headermodification: opts.headerModification || [], //structured list
|
|
153
|
-
httpsOnly: opts.httpsOnly ?? false,
|
|
154
|
-
internalwebdebuggerport: 0,
|
|
155
|
-
ipwhitelist: opts.ipWhitelist
|
|
156
|
-
? (Array.isArray(opts.ipWhitelist)
|
|
157
|
-
? opts.ipWhitelist
|
|
158
|
-
: JSON.parse(opts.ipWhitelist) as string[])
|
|
159
|
-
: null,
|
|
160
|
-
localport: parsedLocalPort || 0,
|
|
161
|
-
localservertlssni: null,
|
|
162
|
-
regioncode: "",
|
|
163
|
-
noReverseProxy: opts.reverseProxy ?? false,
|
|
164
|
-
serveraddress: opts.serverAddress || "free.pinggy.io",
|
|
165
|
-
serverport: 0,
|
|
166
|
-
statusCheckInterval: 0,
|
|
167
|
-
token: opts.token || "",
|
|
168
|
-
tunnelTimeout: 0,
|
|
169
|
-
type: tunnelType,
|
|
170
|
-
webdebuggerport: Number(opts.webDebugger?.split(":")[0]) || 0,
|
|
171
|
-
xff: opts.xForwardedFor ? "1" : "",
|
|
172
|
-
localsservertls: localserverTls || false,
|
|
173
|
-
additionalForwarding: additionalForwarding || [],
|
|
174
|
-
serve: serve || "",
|
|
175
|
-
};
|
|
176
|
-
}
|