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
|
@@ -0,0 +1,2157 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IPCClient,
|
|
3
|
+
SessionMode
|
|
4
|
+
} from "./chunk-BFARGPGP.js";
|
|
5
|
+
import {
|
|
6
|
+
TunnelAlreadyRunningError,
|
|
7
|
+
TunnelManager,
|
|
8
|
+
errorMessage,
|
|
9
|
+
getLocalAddress,
|
|
10
|
+
getVersion,
|
|
11
|
+
isValidPort,
|
|
12
|
+
printer_default
|
|
13
|
+
} from "./chunk-DLNUDW6G.js";
|
|
14
|
+
import {
|
|
15
|
+
logger
|
|
16
|
+
} from "./chunk-7G6SJEEA.js";
|
|
17
|
+
import {
|
|
18
|
+
getDaemonInfoPath,
|
|
19
|
+
getDaemonLogPath
|
|
20
|
+
} from "./chunk-GBYF2H4H.js";
|
|
21
|
+
|
|
22
|
+
// src/types.ts
|
|
23
|
+
var TunnelStateType = /* @__PURE__ */ ((TunnelStateType2) => {
|
|
24
|
+
TunnelStateType2["New"] = "idle";
|
|
25
|
+
TunnelStateType2["Starting"] = "starting";
|
|
26
|
+
TunnelStateType2["Running"] = "running";
|
|
27
|
+
TunnelStateType2["Live"] = "live";
|
|
28
|
+
TunnelStateType2["Closed"] = "closed";
|
|
29
|
+
TunnelStateType2["Exited"] = "exited";
|
|
30
|
+
return TunnelStateType2;
|
|
31
|
+
})(TunnelStateType || {});
|
|
32
|
+
var TunnelErrorCodeType = /* @__PURE__ */ ((TunnelErrorCodeType2) => {
|
|
33
|
+
TunnelErrorCodeType2["NonResponsive"] = "non_responsive";
|
|
34
|
+
TunnelErrorCodeType2["FailedToConnect"] = "failed_to_connect";
|
|
35
|
+
TunnelErrorCodeType2["ErrorInAdditionalForwarding"] = "additional_forwarding_error";
|
|
36
|
+
TunnelErrorCodeType2["WebdebuggerError"] = "webdebugger_error";
|
|
37
|
+
TunnelErrorCodeType2["NoError"] = "";
|
|
38
|
+
return TunnelErrorCodeType2;
|
|
39
|
+
})(TunnelErrorCodeType || {});
|
|
40
|
+
var TunnelWarningCode = /* @__PURE__ */ ((TunnelWarningCode2) => {
|
|
41
|
+
TunnelWarningCode2["InvalidTunnelServePath"] = "INVALID_TUNNEL_SERVE_PATH";
|
|
42
|
+
TunnelWarningCode2["UnknownWarning"] = "UNKNOWN_WARNING";
|
|
43
|
+
return TunnelWarningCode2;
|
|
44
|
+
})(TunnelWarningCode || {});
|
|
45
|
+
var ErrorCode = {
|
|
46
|
+
InvalidRequestMethodError: "INVALID_REQUEST_METHOD",
|
|
47
|
+
InvalidRequestBodyError: "COULD_NOT_READ_BODY",
|
|
48
|
+
InternalServerError: "INTERNAL_SERVER_ERROR",
|
|
49
|
+
InvalidBodyFormatError: "INVALID_DATA_FORMAT",
|
|
50
|
+
ErrorStartingTunnel: "ERROR_STARTING_TUNNEL",
|
|
51
|
+
TunnelNotFound: "TUNNEL_WITH_ID_OR_CONFIG_ID_NOT_FOUND",
|
|
52
|
+
TunnelAlreadyRunningError: "TUNNEL_WITH_ID_OR_CONFIG_ID_ALREADY_RUNNING",
|
|
53
|
+
WebsocketUpgradeFailError: "WEBSOCKET_UPGRADE_FAILED",
|
|
54
|
+
RemoteManagementAlreadyRunning: "REMOTE_MANAGEMENT_ALREADY_RUNNING",
|
|
55
|
+
RemoteManagementNotRunning: "REMOTE_MANAGEMENT_NOT_RUNNING",
|
|
56
|
+
RemoteManagementDeserializationFailed: "REMOTE_MANAGEMENT_DESERIALIZATION_FAILED"
|
|
57
|
+
};
|
|
58
|
+
function isErrorResponse(obj) {
|
|
59
|
+
return typeof obj === "object" && obj !== null && "code" in obj && "message" in obj && typeof obj.message === "string" && Object.values(ErrorCode).includes(obj.code);
|
|
60
|
+
}
|
|
61
|
+
function newErrorResponse(codeOrError, message) {
|
|
62
|
+
if (typeof codeOrError === "object") {
|
|
63
|
+
return codeOrError;
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
code: codeOrError,
|
|
67
|
+
message
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function NewResponseObject(data) {
|
|
71
|
+
const encoder = new TextEncoder();
|
|
72
|
+
const bytes = encoder.encode(JSON.stringify(data));
|
|
73
|
+
return {
|
|
74
|
+
response: bytes,
|
|
75
|
+
requestid: "",
|
|
76
|
+
command: "",
|
|
77
|
+
error: false,
|
|
78
|
+
errorresponse: {}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function NewErrorResponseObject(errorResponse) {
|
|
82
|
+
return {
|
|
83
|
+
response: new Uint8Array(),
|
|
84
|
+
requestid: "",
|
|
85
|
+
command: "",
|
|
86
|
+
error: true,
|
|
87
|
+
errorresponse: errorResponse
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function newStatus(tunnelState, errorCode, errorMsg) {
|
|
91
|
+
let assignedState = tunnelState;
|
|
92
|
+
if (tunnelState === "live" /* Live */) {
|
|
93
|
+
assignedState = "running" /* Running */;
|
|
94
|
+
} else if (tunnelState === "idle" /* New */) {
|
|
95
|
+
assignedState = "idle" /* New */;
|
|
96
|
+
} else if (tunnelState === "closed" /* Closed */) {
|
|
97
|
+
assignedState = "exited" /* Exited */;
|
|
98
|
+
}
|
|
99
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
100
|
+
return {
|
|
101
|
+
state: assignedState,
|
|
102
|
+
errorcode: errorCode,
|
|
103
|
+
errormsg: errorMsg,
|
|
104
|
+
createdtimestamp: now,
|
|
105
|
+
starttimestamp: now,
|
|
106
|
+
endtimestamp: now,
|
|
107
|
+
warnings: []
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function newStats() {
|
|
111
|
+
return {
|
|
112
|
+
numLiveConnections: 0,
|
|
113
|
+
numTotalConnections: 0,
|
|
114
|
+
numTotalReqBytes: 0,
|
|
115
|
+
numTotalResBytes: 0,
|
|
116
|
+
numTotalTxBytes: 0,
|
|
117
|
+
elapsedTime: 0
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
var RemoteManagementStatus = {
|
|
121
|
+
Connecting: "CONNECTING",
|
|
122
|
+
Disconnecting: "DISCONNECTING",
|
|
123
|
+
Reconnecting: "RECONNECTING",
|
|
124
|
+
Running: "RUNNING",
|
|
125
|
+
NotRunning: "NOT_RUNNING",
|
|
126
|
+
Error: "ERROR"
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// src/remote_management/remote_schema.ts
|
|
130
|
+
import { TunnelType } from "@pinggy/pinggy";
|
|
131
|
+
import { z } from "zod";
|
|
132
|
+
var HeaderModificationSchema = z.object({
|
|
133
|
+
key: z.string(),
|
|
134
|
+
value: z.array(z.string()).nullable().optional(),
|
|
135
|
+
type: z.enum(["add", "remove", "update"])
|
|
136
|
+
});
|
|
137
|
+
var AdditionalForwardingSchema = z.object({
|
|
138
|
+
remoteDomain: z.string().optional(),
|
|
139
|
+
remotePort: z.number().optional(),
|
|
140
|
+
localDomain: z.string(),
|
|
141
|
+
localPort: z.number()
|
|
142
|
+
});
|
|
143
|
+
var TunnelConfigSchema = z.object({
|
|
144
|
+
allowPreflight: z.boolean().optional(),
|
|
145
|
+
// primary key
|
|
146
|
+
allowpreflight: z.boolean().optional(),
|
|
147
|
+
// legacy key
|
|
148
|
+
autoreconnect: z.boolean(),
|
|
149
|
+
basicauth: z.array(z.object({ username: z.string(), password: z.string() })).nullable(),
|
|
150
|
+
bearerauth: z.array(z.string()).nullable(),
|
|
151
|
+
configid: z.string(),
|
|
152
|
+
configname: z.string(),
|
|
153
|
+
greetmsg: z.string().optional(),
|
|
154
|
+
force: z.boolean(),
|
|
155
|
+
forwardedhost: z.string(),
|
|
156
|
+
fullRequestUrl: z.boolean(),
|
|
157
|
+
headermodification: z.array(HeaderModificationSchema),
|
|
158
|
+
httpsOnly: z.boolean(),
|
|
159
|
+
internalwebdebuggerport: z.number(),
|
|
160
|
+
ipwhitelist: z.array(z.string()).nullable(),
|
|
161
|
+
localport: z.number(),
|
|
162
|
+
localsservertls: z.union([z.boolean(), z.string()]),
|
|
163
|
+
localservertlssni: z.string().nullable(),
|
|
164
|
+
regioncode: z.string(),
|
|
165
|
+
noReverseProxy: z.boolean(),
|
|
166
|
+
serveraddress: z.string(),
|
|
167
|
+
serverport: z.number(),
|
|
168
|
+
statusCheckInterval: z.number(),
|
|
169
|
+
token: z.string(),
|
|
170
|
+
tunnelTimeout: z.number(),
|
|
171
|
+
type: z.enum([
|
|
172
|
+
TunnelType.Http,
|
|
173
|
+
TunnelType.Tcp,
|
|
174
|
+
TunnelType.Udp,
|
|
175
|
+
TunnelType.Tls,
|
|
176
|
+
TunnelType.TlsTcp
|
|
177
|
+
]),
|
|
178
|
+
webdebuggerport: z.number(),
|
|
179
|
+
xff: z.string(),
|
|
180
|
+
additionalForwarding: z.array(AdditionalForwardingSchema).optional(),
|
|
181
|
+
serve: z.string().optional()
|
|
182
|
+
}).superRefine((data, ctx) => {
|
|
183
|
+
if (data.allowPreflight === void 0 && data.allowpreflight === void 0) {
|
|
184
|
+
ctx.addIssue({
|
|
185
|
+
code: "custom",
|
|
186
|
+
message: "Either allowPreflight or allowpreflight is required",
|
|
187
|
+
path: ["allowPreflight"]
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}).transform((data) => ({
|
|
191
|
+
...data,
|
|
192
|
+
allowPreflight: data.allowPreflight ?? data.allowpreflight,
|
|
193
|
+
allowpreflight: data.allowPreflight ?? data.allowpreflight
|
|
194
|
+
}));
|
|
195
|
+
var StartSchema = z.object({
|
|
196
|
+
tunnelID: z.string().nullable().optional(),
|
|
197
|
+
tunnelConfig: TunnelConfigSchema
|
|
198
|
+
});
|
|
199
|
+
var StopSchema = z.object({
|
|
200
|
+
tunnelID: z.string().min(1)
|
|
201
|
+
});
|
|
202
|
+
var GetSchema = StopSchema;
|
|
203
|
+
var RestartSchema = StopSchema;
|
|
204
|
+
var UpdateConfigSchema = z.object({
|
|
205
|
+
tunnelConfig: TunnelConfigSchema
|
|
206
|
+
});
|
|
207
|
+
var ForwardingEntryV2Schema = z.object({
|
|
208
|
+
listenAddress: z.string().optional(),
|
|
209
|
+
address: z.string(),
|
|
210
|
+
type: z.enum([TunnelType.Http, TunnelType.Tcp, TunnelType.Udp, TunnelType.Tls, TunnelType.TlsTcp])
|
|
211
|
+
});
|
|
212
|
+
var TunnelConfigV1Schema = z.object({
|
|
213
|
+
// Meta Info
|
|
214
|
+
version: z.string(),
|
|
215
|
+
name: z.string(),
|
|
216
|
+
configId: z.string(),
|
|
217
|
+
// General tunnel configurations
|
|
218
|
+
serverAddress: z.string().optional(),
|
|
219
|
+
token: z.string().optional(),
|
|
220
|
+
autoReconnect: z.boolean().optional(),
|
|
221
|
+
reconnectInterval: z.number().optional(),
|
|
222
|
+
maxReconnectAttempts: z.number().optional(),
|
|
223
|
+
force: z.boolean(),
|
|
224
|
+
keepAliveInterval: z.number().optional(),
|
|
225
|
+
webDebugger: z.string(),
|
|
226
|
+
//Forwarding
|
|
227
|
+
// Either a URL string (e.g. "https://localhost:5555") or an array of forwarding entries.
|
|
228
|
+
forwarding: z.union([
|
|
229
|
+
z.string(),
|
|
230
|
+
z.array(ForwardingEntryV2Schema)
|
|
231
|
+
]),
|
|
232
|
+
// IP whitelist
|
|
233
|
+
ipWhitelist: z.array(z.string()).optional(),
|
|
234
|
+
basicAuth: z.array(z.object({ username: z.string(), password: z.string() })).optional(),
|
|
235
|
+
bearerTokenAuth: z.array(z.string()).optional(),
|
|
236
|
+
headerModification: z.array(HeaderModificationSchema).optional(),
|
|
237
|
+
reverseProxy: z.boolean().optional(),
|
|
238
|
+
xForwardedFor: z.boolean().optional(),
|
|
239
|
+
httpsOnly: z.boolean().optional(),
|
|
240
|
+
originalRequestUrl: z.boolean().optional(),
|
|
241
|
+
allowPreflight: z.boolean().optional(),
|
|
242
|
+
serve: z.string().optional(),
|
|
243
|
+
optional: z.record(z.string(), z.unknown()).optional()
|
|
244
|
+
});
|
|
245
|
+
var StartV2Schema = z.object({
|
|
246
|
+
tunnelID: z.string().nullable().optional(),
|
|
247
|
+
tunnelConfig: TunnelConfigV1Schema
|
|
248
|
+
});
|
|
249
|
+
var UpdateConfigV2Schema = z.object({
|
|
250
|
+
tunnelConfig: TunnelConfigV1Schema
|
|
251
|
+
});
|
|
252
|
+
function pinggyOptionsToTunnelConfigV1(opts, configStoredInCli) {
|
|
253
|
+
const parsedTokens = opts.bearerTokenAuth ? Array.isArray(opts.bearerTokenAuth) ? opts.bearerTokenAuth : JSON.parse(opts.bearerTokenAuth) : [];
|
|
254
|
+
return {
|
|
255
|
+
version: configStoredInCli.version || "1.0",
|
|
256
|
+
name: configStoredInCli.name || "",
|
|
257
|
+
configId: configStoredInCli.configId || "",
|
|
258
|
+
serverAddress: opts.serverAddress || "a.pinggy.io:443",
|
|
259
|
+
token: opts.token || "",
|
|
260
|
+
autoReconnect: opts.autoReconnect ?? true,
|
|
261
|
+
force: opts.force ?? false,
|
|
262
|
+
webDebugger: opts.webDebugger || "",
|
|
263
|
+
forwarding: opts.forwarding ? opts.forwarding : "",
|
|
264
|
+
ipWhitelist: opts.ipWhitelist ? Array.isArray(opts.ipWhitelist) ? opts.ipWhitelist : JSON.parse(opts.ipWhitelist) : [],
|
|
265
|
+
basicAuth: opts.basicAuth && Object.keys(opts.basicAuth).length ? opts.basicAuth : void 0,
|
|
266
|
+
bearerTokenAuth: parsedTokens.length ? parsedTokens : void 0,
|
|
267
|
+
headerModification: opts.headerModification || [],
|
|
268
|
+
reverseProxy: opts.reverseProxy ?? false,
|
|
269
|
+
xForwardedFor: !!opts.xForwardedFor,
|
|
270
|
+
httpsOnly: opts.httpsOnly ?? false,
|
|
271
|
+
originalRequestUrl: opts.originalRequestUrl ?? false,
|
|
272
|
+
allowPreflight: opts.allowPreflight ?? false,
|
|
273
|
+
optional: opts.optional || {}
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function tunnelConfigToPinggyOptions(config) {
|
|
277
|
+
const forwardingData = [];
|
|
278
|
+
forwardingData.push({
|
|
279
|
+
address: `${config.forwardedhost}:${config.localport}`,
|
|
280
|
+
type: config.type || TunnelType.Http
|
|
281
|
+
// Default to HTTP for the primary forwarding entry
|
|
282
|
+
});
|
|
283
|
+
if (config.additionalForwarding && Array.isArray(config.additionalForwarding)) {
|
|
284
|
+
config.additionalForwarding.forEach((entry) => {
|
|
285
|
+
if (entry.localDomain && entry.localPort && entry.remoteDomain) {
|
|
286
|
+
const listenAddress = entry.remotePort && isValidPort(entry.remotePort) ? `${entry.remoteDomain}:${entry.remotePort}` : entry.remoteDomain;
|
|
287
|
+
forwardingData.push({
|
|
288
|
+
address: `${entry.localDomain}:${entry.localPort}`,
|
|
289
|
+
listenAddress,
|
|
290
|
+
type: TunnelType.Http
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
return {
|
|
296
|
+
token: config.token || "",
|
|
297
|
+
serverAddress: config.serveraddress || "free.pinggy.io",
|
|
298
|
+
forwarding: forwardingData,
|
|
299
|
+
webDebugger: config.webdebuggerport ? `localhost:${config.webdebuggerport}` : "",
|
|
300
|
+
ipWhitelist: config.ipwhitelist || [],
|
|
301
|
+
basicAuth: config.basicauth ? config.basicauth : [],
|
|
302
|
+
bearerTokenAuth: config.bearerauth || [],
|
|
303
|
+
headerModification: config.headermodification,
|
|
304
|
+
xForwardedFor: !!config.xff,
|
|
305
|
+
httpsOnly: config.httpsOnly,
|
|
306
|
+
originalRequestUrl: config.fullRequestUrl,
|
|
307
|
+
allowPreflight: config.allowPreflight,
|
|
308
|
+
reverseProxy: config.noReverseProxy,
|
|
309
|
+
force: config.force,
|
|
310
|
+
autoReconnect: config.autoreconnect,
|
|
311
|
+
optional: {
|
|
312
|
+
sniServerName: config.localservertlssni || ""
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
function pinggyOptionsToTunnelConfig(opts, configid, configName, localserverTls, greetMsg, serve) {
|
|
317
|
+
let primaryEntry;
|
|
318
|
+
let additionalEntries = [];
|
|
319
|
+
if (Array.isArray(opts.forwarding)) {
|
|
320
|
+
primaryEntry = opts.forwarding.find((e) => !e.listenAddress) ?? opts.forwarding[0];
|
|
321
|
+
additionalEntries = opts.forwarding.filter(
|
|
322
|
+
(e) => e !== primaryEntry && Boolean(e.listenAddress)
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
const forwarding = primaryEntry ? String(primaryEntry.address) : String(opts.forwarding);
|
|
326
|
+
const [parsedForwardedHost, portStr] = forwarding.split(":");
|
|
327
|
+
const parsedLocalPort = parseInt(portStr, 10);
|
|
328
|
+
const tunnelType = primaryEntry?.type ?? TunnelType.Http;
|
|
329
|
+
const additionalForwarding = additionalEntries.map((e) => {
|
|
330
|
+
const [localDomain, localPortStr] = String(e.address).split(":");
|
|
331
|
+
const [remoteDomain, remotePortStr] = String(e.listenAddress).split(":");
|
|
332
|
+
const localPort = parseInt(localPortStr, 10);
|
|
333
|
+
const remotePort = parseInt(remotePortStr, 10);
|
|
334
|
+
return {
|
|
335
|
+
localDomain,
|
|
336
|
+
localPort: isNaN(localPort) ? 0 : localPort,
|
|
337
|
+
remoteDomain,
|
|
338
|
+
remotePort: isNaN(remotePort) ? 0 : remotePort
|
|
339
|
+
};
|
|
340
|
+
});
|
|
341
|
+
const parsedTokens = opts.bearerTokenAuth ? Array.isArray(opts.bearerTokenAuth) ? opts.bearerTokenAuth : JSON.parse(opts.bearerTokenAuth) : [];
|
|
342
|
+
return {
|
|
343
|
+
allowPreflight: opts.allowPreflight ?? false,
|
|
344
|
+
allowpreflight: opts.allowPreflight ?? false,
|
|
345
|
+
autoreconnect: opts.autoReconnect ?? false,
|
|
346
|
+
basicauth: opts.basicAuth && Object.keys(opts.basicAuth).length ? opts.basicAuth : null,
|
|
347
|
+
bearerauth: parsedTokens.length ? [parsedTokens.join(",")] : null,
|
|
348
|
+
configid,
|
|
349
|
+
configname: configName,
|
|
350
|
+
greetmsg: greetMsg || "",
|
|
351
|
+
force: opts.force ?? false,
|
|
352
|
+
forwardedhost: parsedForwardedHost || "localhost",
|
|
353
|
+
fullRequestUrl: opts.originalRequestUrl ?? false,
|
|
354
|
+
headermodification: opts.headerModification || [],
|
|
355
|
+
//structured list
|
|
356
|
+
httpsOnly: opts.httpsOnly ?? false,
|
|
357
|
+
internalwebdebuggerport: 0,
|
|
358
|
+
ipwhitelist: opts.ipWhitelist ? Array.isArray(opts.ipWhitelist) ? opts.ipWhitelist : JSON.parse(opts.ipWhitelist) : null,
|
|
359
|
+
localport: parsedLocalPort || 0,
|
|
360
|
+
localservertlssni: null,
|
|
361
|
+
regioncode: "",
|
|
362
|
+
noReverseProxy: opts.reverseProxy ?? false,
|
|
363
|
+
serveraddress: opts.serverAddress || "free.pinggy.io",
|
|
364
|
+
serverport: 0,
|
|
365
|
+
statusCheckInterval: 0,
|
|
366
|
+
token: opts.token || "",
|
|
367
|
+
tunnelTimeout: 0,
|
|
368
|
+
type: tunnelType,
|
|
369
|
+
webdebuggerport: Number(opts.webDebugger?.split(":")[0]) || 0,
|
|
370
|
+
xff: opts.xForwardedFor ? "1" : "",
|
|
371
|
+
localsservertls: localserverTls || false,
|
|
372
|
+
additionalForwarding: additionalForwarding || [],
|
|
373
|
+
serve: serve || ""
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// src/remote_management/handler.ts
|
|
378
|
+
var TunnelOperations = class {
|
|
379
|
+
constructor() {
|
|
380
|
+
this.tunnelManager = TunnelManager.getInstance();
|
|
381
|
+
}
|
|
382
|
+
buildStatus(tunnelId, state, errorCode) {
|
|
383
|
+
const status = newStatus(state, errorCode, "");
|
|
384
|
+
try {
|
|
385
|
+
const managed = this.tunnelManager.getManagedTunnel("", tunnelId);
|
|
386
|
+
if (managed) {
|
|
387
|
+
status.createdtimestamp = managed.createdAt || "";
|
|
388
|
+
status.starttimestamp = managed.startedAt || "";
|
|
389
|
+
status.endtimestamp = managed.stoppedAt || "";
|
|
390
|
+
}
|
|
391
|
+
if (managed?.lastError) {
|
|
392
|
+
status.lastError = managed.lastError;
|
|
393
|
+
}
|
|
394
|
+
} catch (e) {
|
|
395
|
+
}
|
|
396
|
+
return status;
|
|
397
|
+
}
|
|
398
|
+
// --- Placeholder response ---
|
|
399
|
+
buildPendingTunnelResponse(tunnelid, tunnelConfig, configid, tunnelName, serve) {
|
|
400
|
+
return {
|
|
401
|
+
tunnelid,
|
|
402
|
+
remoteurls: [],
|
|
403
|
+
tunnelconfig: pinggyOptionsToTunnelConfig(tunnelConfig, configid, tunnelName, false, void 0, serve),
|
|
404
|
+
status: this.buildStatus(tunnelid, "starting" /* Starting */, "" /* NoError */),
|
|
405
|
+
stats: newStats()
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
buildPendingTunnelResponseV2(tunnelid, tunnelConfig, configFromCli, configid, tunnelName, serve) {
|
|
409
|
+
return {
|
|
410
|
+
tunnelid,
|
|
411
|
+
remoteurls: [],
|
|
412
|
+
tunnelconfig: pinggyOptionsToTunnelConfigV1(tunnelConfig, configFromCli),
|
|
413
|
+
status: this.buildStatus(tunnelid, "starting" /* Starting */, "" /* NoError */),
|
|
414
|
+
stats: newStats(),
|
|
415
|
+
greetmsg: ""
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
// --- Helper to construct TunnelResponse ---
|
|
419
|
+
async buildTunnelResponse(tunnelid, tunnelConfig, configid, tunnelName, serve) {
|
|
420
|
+
const stats = this.tunnelManager.getLatestTunnelStats(tunnelid) || newStats();
|
|
421
|
+
const [status, tlsInfo, greetMsg, remoteurls] = await Promise.all([
|
|
422
|
+
this.tunnelManager.getTunnelStatus(tunnelid),
|
|
423
|
+
this.tunnelManager.getLocalserverTlsInfo(tunnelid),
|
|
424
|
+
this.tunnelManager.getTunnelGreetMessage(tunnelid),
|
|
425
|
+
this.tunnelManager.getTunnelUrls(tunnelid)
|
|
426
|
+
]);
|
|
427
|
+
return {
|
|
428
|
+
tunnelid,
|
|
429
|
+
remoteurls,
|
|
430
|
+
tunnelconfig: pinggyOptionsToTunnelConfig(tunnelConfig, configid, tunnelName, tlsInfo, greetMsg),
|
|
431
|
+
status: this.buildStatus(tunnelid, status, "" /* NoError */),
|
|
432
|
+
stats
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
async buildTunnelResponseV2(tunnelid, tunnelConfig, configFromCli, configid, tunnelName, serve) {
|
|
436
|
+
const stats = this.tunnelManager.getLatestTunnelStats(tunnelid) || newStats();
|
|
437
|
+
const [status, greetMsg, remoteurls] = await Promise.all([
|
|
438
|
+
this.tunnelManager.getTunnelStatus(tunnelid),
|
|
439
|
+
this.tunnelManager.getTunnelGreetMessage(tunnelid),
|
|
440
|
+
this.tunnelManager.getTunnelUrls(tunnelid)
|
|
441
|
+
]);
|
|
442
|
+
return {
|
|
443
|
+
tunnelid,
|
|
444
|
+
remoteurls,
|
|
445
|
+
tunnelconfig: pinggyOptionsToTunnelConfigV1(tunnelConfig, configFromCli),
|
|
446
|
+
status: this.buildStatus(tunnelid, status, "" /* NoError */),
|
|
447
|
+
stats,
|
|
448
|
+
greetmsg: greetMsg
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
error(code, err, fallback) {
|
|
452
|
+
return newErrorResponse({
|
|
453
|
+
code,
|
|
454
|
+
message: err instanceof Error ? err.message : fallback
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
// --- Operations ---
|
|
458
|
+
async handleStart(config, noWait = false, origin = "cli") {
|
|
459
|
+
try {
|
|
460
|
+
const opts = tunnelConfigToPinggyOptions(config);
|
|
461
|
+
const managed = await this.tunnelManager.createTunnel({
|
|
462
|
+
...opts,
|
|
463
|
+
configId: config.configid,
|
|
464
|
+
name: config.configname,
|
|
465
|
+
optional: {
|
|
466
|
+
serve: config.serve
|
|
467
|
+
}
|
|
468
|
+
}, origin);
|
|
469
|
+
const { tunnelid, tunnelName, serve, tunnelConfig } = managed;
|
|
470
|
+
const startPromise = this.tunnelManager.startTunnel(tunnelid);
|
|
471
|
+
if (noWait) {
|
|
472
|
+
startPromise.catch((err) => {
|
|
473
|
+
logger.error("No-wait startTunnel failed", { tunnelid, err: String(err) });
|
|
474
|
+
});
|
|
475
|
+
return this.buildPendingTunnelResponse(tunnelid, tunnelConfig, config.configid, tunnelName, serve);
|
|
476
|
+
}
|
|
477
|
+
await startPromise;
|
|
478
|
+
const tunnelPconfig = await this.tunnelManager.getTunnelConfig("", tunnelid);
|
|
479
|
+
return this.buildTunnelResponse(tunnelid, tunnelPconfig, config.configid, tunnelName, serve);
|
|
480
|
+
} catch (err) {
|
|
481
|
+
return this.error(ErrorCode.ErrorStartingTunnel, err, "Unknown error occurred while starting tunnel");
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
async handleStartV2(config, noWait = false, origin = "cli") {
|
|
485
|
+
try {
|
|
486
|
+
const managed = await this.tunnelManager.createTunnel(config, origin);
|
|
487
|
+
const { tunnelid, tunnelConfig } = managed;
|
|
488
|
+
const startPromise = this.tunnelManager.startTunnel(tunnelid);
|
|
489
|
+
if (noWait) {
|
|
490
|
+
startPromise.catch((err) => {
|
|
491
|
+
logger.error("No-wait startTunnel failed", { tunnelid, err: String(err) });
|
|
492
|
+
});
|
|
493
|
+
return this.buildPendingTunnelResponseV2(tunnelid, tunnelConfig, config, config.configId, config.name, config.serve);
|
|
494
|
+
}
|
|
495
|
+
await startPromise;
|
|
496
|
+
const tunnelPconfig = await this.tunnelManager.getTunnelConfig("", tunnelid);
|
|
497
|
+
return this.buildTunnelResponseV2(tunnelid, tunnelPconfig, config, config.configId, config.name, config.serve);
|
|
498
|
+
} catch (err) {
|
|
499
|
+
if (err instanceof TunnelAlreadyRunningError) {
|
|
500
|
+
return this.error(ErrorCode.TunnelAlreadyRunningError, err, err.message);
|
|
501
|
+
}
|
|
502
|
+
return this.error(ErrorCode.ErrorStartingTunnel, err, "Unknown error occurred while starting tunnel");
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
async handleUpdateConfig(config, noWait = false) {
|
|
506
|
+
try {
|
|
507
|
+
const opts = tunnelConfigToPinggyOptions(config);
|
|
508
|
+
const updateOpts = {
|
|
509
|
+
...opts,
|
|
510
|
+
configId: config.configid,
|
|
511
|
+
name: config.configname,
|
|
512
|
+
optional: {
|
|
513
|
+
serve: config.serve
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
if (noWait) {
|
|
517
|
+
const existing = this.tunnelManager.getManagedTunnel(config.configid);
|
|
518
|
+
if (!existing.tunnelConfig) throw new Error("Invalid tunnel state before configuration update");
|
|
519
|
+
this.tunnelManager.updateConfig(updateOpts).catch((err) => {
|
|
520
|
+
logger.error("No-wait updateConfig failed", { configid: config.configid, err: String(err) });
|
|
521
|
+
});
|
|
522
|
+
return this.buildPendingTunnelResponse(existing.tunnelid, existing.tunnelConfig, config.configid, existing.tunnelName, existing.serve);
|
|
523
|
+
}
|
|
524
|
+
const tunnel = await this.tunnelManager.updateConfig(updateOpts);
|
|
525
|
+
if (!tunnel.instance || !tunnel.tunnelConfig)
|
|
526
|
+
throw new Error("Invalid tunnel state after configuration update");
|
|
527
|
+
return this.buildTunnelResponse(tunnel.tunnelid, tunnel.tunnelConfig, config.configid, tunnel.tunnelName, tunnel.serve);
|
|
528
|
+
} catch (err) {
|
|
529
|
+
return this.error(ErrorCode.InternalServerError, err, "Failed to update tunnel configuration");
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
async handleUpdateConfigV2(config, noWait = false) {
|
|
533
|
+
try {
|
|
534
|
+
if (noWait) {
|
|
535
|
+
const existing = this.tunnelManager.getManagedTunnel(config.configId);
|
|
536
|
+
if (!existing.tunnelConfig) throw new Error("Invalid tunnel state before configuration update");
|
|
537
|
+
this.tunnelManager.updateConfig(config).catch((err) => {
|
|
538
|
+
logger.error("No-wait updateConfigV2 failed", { configId: config.configId, err: String(err) });
|
|
539
|
+
});
|
|
540
|
+
return this.buildPendingTunnelResponseV2(existing.tunnelid, existing.tunnelConfig, config, config.configId, existing.tunnelName, existing.serve);
|
|
541
|
+
}
|
|
542
|
+
const tunnel = await this.tunnelManager.updateConfig(config);
|
|
543
|
+
if (!tunnel.instance || !tunnel.tunnelConfig)
|
|
544
|
+
throw new Error("Invalid tunnel state after configuration update");
|
|
545
|
+
return this.buildTunnelResponseV2(tunnel.tunnelid, tunnel.tunnelConfig, config, config.configId, tunnel.tunnelName, tunnel.serve);
|
|
546
|
+
} catch (err) {
|
|
547
|
+
return this.error(ErrorCode.InternalServerError, err, "Failed to update tunnel configuration");
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
async handleListV2() {
|
|
551
|
+
try {
|
|
552
|
+
const tunnels = await this.tunnelManager.getAllTunnels();
|
|
553
|
+
if (tunnels.length === 0) {
|
|
554
|
+
return [];
|
|
555
|
+
}
|
|
556
|
+
return Promise.all(
|
|
557
|
+
tunnels.map(async (t) => {
|
|
558
|
+
const rawStats = this.tunnelManager.getLatestTunnelStats(t.tunnelid) || newStats();
|
|
559
|
+
const [status, tlsInfo, greetMsg] = await Promise.all([
|
|
560
|
+
this.tunnelManager.getTunnelStatus(t.tunnelid),
|
|
561
|
+
this.tunnelManager.getLocalserverTlsInfo(t.tunnelid),
|
|
562
|
+
this.tunnelManager.getTunnelGreetMessage(t.tunnelid)
|
|
563
|
+
]);
|
|
564
|
+
const tunnelConfguration = status !== "closed" /* Closed */ && status !== "exited" /* Exited */ ? await this.tunnelManager.getTunnelConfig("", t.tunnelid) : t.tunnelConfig;
|
|
565
|
+
const tunnelConfig = pinggyOptionsToTunnelConfigV1(tunnelConfguration, t.tunnelConfig);
|
|
566
|
+
return {
|
|
567
|
+
tunnelid: t.tunnelid,
|
|
568
|
+
remoteurls: t.remoteurls,
|
|
569
|
+
status: this.buildStatus(t.tunnelid, status, "" /* NoError */),
|
|
570
|
+
stats: rawStats,
|
|
571
|
+
tunnelconfig: tunnelConfig,
|
|
572
|
+
greetmsg: greetMsg
|
|
573
|
+
};
|
|
574
|
+
})
|
|
575
|
+
);
|
|
576
|
+
} catch (err) {
|
|
577
|
+
return this.error(ErrorCode.InternalServerError, err, "Failed to list tunnels");
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
async handleList() {
|
|
581
|
+
try {
|
|
582
|
+
const tunnels = await this.tunnelManager.getAllTunnels();
|
|
583
|
+
if (tunnels.length === 0) {
|
|
584
|
+
return [];
|
|
585
|
+
}
|
|
586
|
+
return Promise.all(
|
|
587
|
+
tunnels.map(async (t) => {
|
|
588
|
+
const rawStats = this.tunnelManager.getLatestTunnelStats(t.tunnelid) || newStats();
|
|
589
|
+
const [status, tlsInfo, greetMsg] = await Promise.all([
|
|
590
|
+
this.tunnelManager.getTunnelStatus(t.tunnelid),
|
|
591
|
+
this.tunnelManager.getLocalserverTlsInfo(t.tunnelid),
|
|
592
|
+
this.tunnelManager.getTunnelGreetMessage(t.tunnelid)
|
|
593
|
+
]);
|
|
594
|
+
const pinggyOptions = status !== "closed" /* Closed */ && status !== "exited" /* Exited */ ? await this.tunnelManager.getTunnelConfig("", t.tunnelid) : t.tunnelConfig;
|
|
595
|
+
const tunnelConfig = pinggyOptionsToTunnelConfig(pinggyOptions, t.configId, t.tunnelName, tlsInfo, greetMsg, t.serve);
|
|
596
|
+
return {
|
|
597
|
+
tunnelid: t.tunnelid,
|
|
598
|
+
remoteurls: t.remoteurls,
|
|
599
|
+
status: this.buildStatus(t.tunnelid, status, "" /* NoError */),
|
|
600
|
+
stats: rawStats,
|
|
601
|
+
tunnelconfig: tunnelConfig
|
|
602
|
+
};
|
|
603
|
+
})
|
|
604
|
+
);
|
|
605
|
+
} catch (err) {
|
|
606
|
+
return this.error(ErrorCode.InternalServerError, err, "Failed to list tunnels");
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
async handleStop(tunnelid) {
|
|
610
|
+
try {
|
|
611
|
+
const { configId } = this.tunnelManager.stopTunnel(tunnelid);
|
|
612
|
+
const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
|
|
613
|
+
if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
|
|
614
|
+
return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, configId, managed.tunnelName, managed.serve);
|
|
615
|
+
} catch (err) {
|
|
616
|
+
return this.error(ErrorCode.TunnelNotFound, err, "Failed to stop tunnel");
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
async handleGet(tunnelid) {
|
|
620
|
+
try {
|
|
621
|
+
const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
|
|
622
|
+
if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
|
|
623
|
+
return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, managed.configId, managed.tunnelName, managed.serve);
|
|
624
|
+
} catch (err) {
|
|
625
|
+
return this.error(ErrorCode.TunnelNotFound, err, "Failed to get tunnel information");
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
async handleRestart(tunnelid, noWait = false) {
|
|
629
|
+
try {
|
|
630
|
+
if (noWait) {
|
|
631
|
+
const managed2 = this.tunnelManager.getManagedTunnel("", tunnelid);
|
|
632
|
+
if (!managed2?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
|
|
633
|
+
this.tunnelManager.restartTunnel(tunnelid).catch((err) => {
|
|
634
|
+
logger.error("No-wait restartTunnel failed", { tunnelid, err: String(err) });
|
|
635
|
+
});
|
|
636
|
+
return this.buildPendingTunnelResponse(tunnelid, managed2.tunnelConfig, managed2.configId, managed2.tunnelName, managed2.serve);
|
|
637
|
+
}
|
|
638
|
+
await this.tunnelManager.restartTunnel(tunnelid);
|
|
639
|
+
const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
|
|
640
|
+
if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
|
|
641
|
+
return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, managed.configId, managed.tunnelName, managed.serve);
|
|
642
|
+
} catch (err) {
|
|
643
|
+
return this.error(ErrorCode.TunnelNotFound, err, "Failed to restart tunnel");
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
handleRegisterStatsListener(tunnelid, listener) {
|
|
647
|
+
void this.tunnelManager.registerStatsListener(tunnelid, listener);
|
|
648
|
+
}
|
|
649
|
+
handleUnregisterStatsListener(tunnelid, listnerId) {
|
|
650
|
+
this.tunnelManager.deregisterStatsListener(tunnelid, listnerId);
|
|
651
|
+
}
|
|
652
|
+
handleGetTunnelStats(tunnelid) {
|
|
653
|
+
try {
|
|
654
|
+
const stats = this.tunnelManager.getTunnelStats(tunnelid);
|
|
655
|
+
if (!stats) {
|
|
656
|
+
return Promise.resolve([newStats()]);
|
|
657
|
+
}
|
|
658
|
+
return Promise.resolve(stats);
|
|
659
|
+
} catch (err) {
|
|
660
|
+
return Promise.resolve(this.error(ErrorCode.TunnelNotFound, err, "Failed to get tunnel stats"));
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
handleRegisterDisconnectListener(tunnelid, listener) {
|
|
664
|
+
void this.tunnelManager.registerDisconnectListener(tunnelid, listener);
|
|
665
|
+
}
|
|
666
|
+
handleRemoveStoppedTunnelByConfigId(configId) {
|
|
667
|
+
try {
|
|
668
|
+
return this.tunnelManager.removeStoppedTunnelByConfigId(configId);
|
|
669
|
+
} catch (err) {
|
|
670
|
+
return this.error(ErrorCode.InternalServerError, err, "Failed to remove stopped tunnel by configId");
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
handleRemoveStoppedTunnelByTunnelId(tunnelId) {
|
|
674
|
+
try {
|
|
675
|
+
return this.tunnelManager.removeStoppedTunnelByTunnelId(tunnelId);
|
|
676
|
+
} catch (err) {
|
|
677
|
+
return this.error(ErrorCode.InternalServerError, err, "Failed to remove stopped tunnel by tunnelId");
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
// src/remote_management/remoteManagement.ts
|
|
683
|
+
import WebSocket from "ws";
|
|
684
|
+
|
|
685
|
+
// src/remote_management/websocket_printer.ts
|
|
686
|
+
import pico from "picocolors";
|
|
687
|
+
var PENDING_START_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
688
|
+
var RemoteManagementWebSocketPrinter = class {
|
|
689
|
+
constructor() {
|
|
690
|
+
this.pendingStarts = /* @__PURE__ */ new Map();
|
|
691
|
+
}
|
|
692
|
+
setTunnelHandler(tunnelHandler) {
|
|
693
|
+
this.tunnelHandler = tunnelHandler;
|
|
694
|
+
}
|
|
695
|
+
queueStart(config) {
|
|
696
|
+
this.cleanupExpiredPendingStarts();
|
|
697
|
+
const entry = {
|
|
698
|
+
configId: this.getConfigIdFromRequest(config),
|
|
699
|
+
configName: this.getConfigNameFromRequest(config),
|
|
700
|
+
queuedAt: Date.now()
|
|
701
|
+
};
|
|
702
|
+
this.latestPendingConfigId = entry.configId;
|
|
703
|
+
this.pendingStarts.set(entry.configId, entry);
|
|
704
|
+
printer_default.startSpinner("Starting tunnel with config name: " + entry.configName);
|
|
705
|
+
}
|
|
706
|
+
failQueuedStart(config, reason) {
|
|
707
|
+
const configId = this.getConfigIdFromRequest(config);
|
|
708
|
+
const pending = this.pendingStarts.get(configId);
|
|
709
|
+
const configName = pending?.configName || this.getConfigNameFromRequest(config);
|
|
710
|
+
this.pendingStarts.delete(configId);
|
|
711
|
+
if (this.latestPendingConfigId === configId) {
|
|
712
|
+
this.latestPendingConfigId = void 0;
|
|
713
|
+
printer_default.stopSpinnerFail(`Failed to start tunnel with config name: ${configName}. ${reason}`);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
handleStartResult(config, result) {
|
|
717
|
+
this.cleanupExpiredPendingStarts();
|
|
718
|
+
const requestedConfigId = this.getConfigIdFromRequest(config);
|
|
719
|
+
if (this.latestPendingConfigId && requestedConfigId !== this.latestPendingConfigId) {
|
|
720
|
+
this.pendingStarts.delete(requestedConfigId);
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
if (isErrorResponse(result)) {
|
|
724
|
+
this.failQueuedStart(config, result.message);
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
const configId = this.getConfigIdFromTunnel(result);
|
|
728
|
+
const pending = this.pendingStarts.get(requestedConfigId) || {
|
|
729
|
+
configId: requestedConfigId,
|
|
730
|
+
configName: this.getConfigNameFromRequest(config),
|
|
731
|
+
queuedAt: Date.now()
|
|
732
|
+
};
|
|
733
|
+
pending.tunnelId = result.tunnelid;
|
|
734
|
+
this.pendingStarts.set(requestedConfigId, pending);
|
|
735
|
+
if (result.remoteurls.length > 0) {
|
|
736
|
+
this.completePendingStart(pending, result.remoteurls);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
printStopRequested(tunnelId) {
|
|
740
|
+
const details = this.resolveTunnelDetails(tunnelId);
|
|
741
|
+
printer_default.startSpinner("Stopping tunnel with config name: " + details.configName);
|
|
742
|
+
}
|
|
743
|
+
handleStopResult(tunnelId, result) {
|
|
744
|
+
const details = this.resolveTunnelDetails(tunnelId, result);
|
|
745
|
+
if (isErrorResponse(result)) {
|
|
746
|
+
printer_default.stopSpinnerFail("Failed to stop tunnel with config name: " + details.configName);
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
this.pendingStarts.delete(details.configId);
|
|
750
|
+
printer_default.stopSpinnerSuccess("Stopped tunnel with config name: " + details.configName);
|
|
751
|
+
}
|
|
752
|
+
printRestartRequested(tunnelId) {
|
|
753
|
+
const details = this.resolveTunnelDetails(tunnelId);
|
|
754
|
+
printer_default.startSpinner("Restarting tunnel with config name: " + details.configName);
|
|
755
|
+
}
|
|
756
|
+
handleRestartResult(tunnelId, result) {
|
|
757
|
+
const details = this.resolveTunnelDetails(tunnelId, result);
|
|
758
|
+
if (isErrorResponse(result)) {
|
|
759
|
+
printer_default.warn(`Failed to restart tunnel with config name: ${details.configName}. ${result.message}`);
|
|
760
|
+
printer_default.stopSpinnerFail("Failed to restart tunnel with config name: " + details.configName);
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
printer_default.stopSpinnerSuccess("Restarted tunnel with config name: " + details.configName);
|
|
764
|
+
if (result.remoteurls?.length > 0) {
|
|
765
|
+
printer_default.info(pico.cyanBright("Remote URLs:"));
|
|
766
|
+
(result.remoteurls ?? []).forEach(
|
|
767
|
+
(url) => printer_default.print(" " + pico.magentaBright(url))
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
monitorList(result) {
|
|
772
|
+
this.cleanupExpiredPendingStarts();
|
|
773
|
+
if (!Array.isArray(result) || this.pendingStarts.size === 0 || !this.latestPendingConfigId) {
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
for (const tunnel of result) {
|
|
777
|
+
const pending = this.findPendingStart(tunnel);
|
|
778
|
+
if (!pending) {
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
if (pending.configId !== this.latestPendingConfigId) {
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
pending.tunnelId = tunnel.tunnelid;
|
|
785
|
+
this.pendingStarts.set(pending.configId, pending);
|
|
786
|
+
if (tunnel.remoteurls.length > 0) {
|
|
787
|
+
this.completePendingStart(pending, tunnel.remoteurls);
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
if (tunnel.status.state === "exited" /* Exited */) {
|
|
791
|
+
const reason = tunnel.status.errormsg || "Tunnel exited before a public URL was assigned";
|
|
792
|
+
this.pendingStarts.delete(pending.configId);
|
|
793
|
+
this.latestPendingConfigId = void 0;
|
|
794
|
+
printer_default.stopSpinnerFail(`Tunnel start did not complete for config name: ${pending.configName}. ${reason}`);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
completePendingStart(entry, urls) {
|
|
799
|
+
if (this.latestPendingConfigId && entry.configId !== this.latestPendingConfigId) {
|
|
800
|
+
this.pendingStarts.delete(entry.configId);
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
this.pendingStarts.delete(entry.configId);
|
|
804
|
+
this.latestPendingConfigId = void 0;
|
|
805
|
+
printer_default.stopSpinnerSuccess(`Tunnel started with config name: ${entry.configName}.`);
|
|
806
|
+
printer_default.info(pico.cyanBright("Remote URLs:"));
|
|
807
|
+
(urls ?? []).forEach(
|
|
808
|
+
(url) => printer_default.print(" " + pico.magentaBright(url))
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
cleanupExpiredPendingStarts() {
|
|
812
|
+
const now = Date.now();
|
|
813
|
+
for (const [configId, entry] of this.pendingStarts.entries()) {
|
|
814
|
+
if (now - entry.queuedAt <= PENDING_START_TIMEOUT_MS) {
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
this.pendingStarts.delete(configId);
|
|
818
|
+
printer_default.warn(`Timed out while waiting for tunnel URL for config name: ${entry.configName}`);
|
|
819
|
+
logger.warn("Pending websocket start entry expired", { configId, tunnelId: entry.tunnelId });
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
findPendingStart(tunnel) {
|
|
823
|
+
const configId = this.getConfigIdFromTunnel(tunnel);
|
|
824
|
+
const byConfigId = this.pendingStarts.get(configId);
|
|
825
|
+
if (byConfigId) {
|
|
826
|
+
return byConfigId;
|
|
827
|
+
}
|
|
828
|
+
for (const entry of this.pendingStarts.values()) {
|
|
829
|
+
if (entry.tunnelId === tunnel.tunnelid) {
|
|
830
|
+
return entry;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
return void 0;
|
|
834
|
+
}
|
|
835
|
+
resolveTunnelDetails(tunnelId, result) {
|
|
836
|
+
if (result && !isErrorResponse(result)) {
|
|
837
|
+
return {
|
|
838
|
+
configId: this.getConfigIdFromTunnel(result),
|
|
839
|
+
configName: this.getConfigNameFromTunnel(result)
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
return {
|
|
843
|
+
configId: tunnelId,
|
|
844
|
+
configName: tunnelId
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
getConfigIdFromRequest(config) {
|
|
848
|
+
return "configid" in config ? config.configid : config.configId;
|
|
849
|
+
}
|
|
850
|
+
getConfigNameFromRequest(config) {
|
|
851
|
+
return "configname" in config ? config.configname : config.name;
|
|
852
|
+
}
|
|
853
|
+
getConfigIdFromTunnel(tunnel) {
|
|
854
|
+
return "configid" in tunnel.tunnelconfig ? tunnel.tunnelconfig.configid : tunnel.tunnelconfig.configId;
|
|
855
|
+
}
|
|
856
|
+
getConfigNameFromTunnel(tunnel) {
|
|
857
|
+
return "configname" in tunnel.tunnelconfig ? tunnel.tunnelconfig.configname : tunnel.tunnelconfig.name;
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
var remoteManagementWebSocketPrinter = new RemoteManagementWebSocketPrinter();
|
|
861
|
+
|
|
862
|
+
// src/remote_management/websocket_handlers.ts
|
|
863
|
+
import z2 from "zod";
|
|
864
|
+
var WsCommand = {
|
|
865
|
+
Start: "start",
|
|
866
|
+
StartV2: "start-v2",
|
|
867
|
+
Stop: "stop",
|
|
868
|
+
Get: "get",
|
|
869
|
+
Restart: "restart",
|
|
870
|
+
UpdateConfig: "updateconfig",
|
|
871
|
+
UpdateConfigV2: "update-config-v2",
|
|
872
|
+
List: "list",
|
|
873
|
+
ListV2: "list-v2",
|
|
874
|
+
GetVersion: "get-version"
|
|
875
|
+
};
|
|
876
|
+
var WebSocketCommandHandler = class {
|
|
877
|
+
constructor(handler) {
|
|
878
|
+
this.tunnelHandler = handler ?? new TunnelOperations();
|
|
879
|
+
remoteManagementWebSocketPrinter.setTunnelHandler(this.tunnelHandler);
|
|
880
|
+
}
|
|
881
|
+
safeParse(text) {
|
|
882
|
+
if (!text) return void 0;
|
|
883
|
+
try {
|
|
884
|
+
return JSON.parse(text);
|
|
885
|
+
} catch (e) {
|
|
886
|
+
logger.warn("Invalid JSON payload", { error: String(e), text });
|
|
887
|
+
return void 0;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
sendResponse(ws, resp) {
|
|
891
|
+
const payload = {
|
|
892
|
+
...resp,
|
|
893
|
+
response: Buffer.from(resp.response || []).toString("base64")
|
|
894
|
+
};
|
|
895
|
+
ws.send(JSON.stringify(payload));
|
|
896
|
+
}
|
|
897
|
+
sendError(ws, req, message, code = ErrorCode.InternalServerError) {
|
|
898
|
+
const resp = NewErrorResponseObject({ code, message });
|
|
899
|
+
resp.command = req.command || "";
|
|
900
|
+
resp.requestid = req.requestid || "";
|
|
901
|
+
this.sendResponse(ws, resp);
|
|
902
|
+
}
|
|
903
|
+
async handleStartReq(req, raw) {
|
|
904
|
+
let queuedConfig;
|
|
905
|
+
try {
|
|
906
|
+
const dc = StartSchema.parse(raw);
|
|
907
|
+
queuedConfig = dc.tunnelConfig;
|
|
908
|
+
remoteManagementWebSocketPrinter.queueStart(dc.tunnelConfig);
|
|
909
|
+
const result = await this.tunnelHandler.handleStart(dc.tunnelConfig, true);
|
|
910
|
+
remoteManagementWebSocketPrinter.handleStartResult(dc.tunnelConfig, result);
|
|
911
|
+
return this.wrapResponse(result, req);
|
|
912
|
+
} catch (e) {
|
|
913
|
+
if (queuedConfig) {
|
|
914
|
+
remoteManagementWebSocketPrinter.failQueuedStart(queuedConfig, String(e));
|
|
915
|
+
}
|
|
916
|
+
if (e instanceof z2.ZodError) {
|
|
917
|
+
printer_default.warn("Validation failed for start request");
|
|
918
|
+
return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
|
|
919
|
+
}
|
|
920
|
+
printer_default.warn(`Error in handleStartReq error: ${String(e)}`);
|
|
921
|
+
return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
async handleStartV2Req(req, raw) {
|
|
925
|
+
let queuedConfig;
|
|
926
|
+
try {
|
|
927
|
+
const dc = StartV2Schema.parse(raw);
|
|
928
|
+
queuedConfig = dc.tunnelConfig;
|
|
929
|
+
remoteManagementWebSocketPrinter.queueStart(dc.tunnelConfig);
|
|
930
|
+
const result = await this.tunnelHandler.handleStartV2(dc.tunnelConfig, true);
|
|
931
|
+
remoteManagementWebSocketPrinter.handleStartResult(dc.tunnelConfig, result);
|
|
932
|
+
return this.wrapResponse(result, req);
|
|
933
|
+
} catch (e) {
|
|
934
|
+
if (queuedConfig) {
|
|
935
|
+
remoteManagementWebSocketPrinter.failQueuedStart(queuedConfig, String(e));
|
|
936
|
+
}
|
|
937
|
+
if (e instanceof z2.ZodError) {
|
|
938
|
+
printer_default.warn("Validation failed for start-v2 request");
|
|
939
|
+
return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
|
|
940
|
+
}
|
|
941
|
+
printer_default.warn(`Error in handleStartV2Req error: ${String(e)}`);
|
|
942
|
+
return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
async handleStopReq(req, raw) {
|
|
946
|
+
try {
|
|
947
|
+
const dc = StopSchema.parse(raw);
|
|
948
|
+
remoteManagementWebSocketPrinter.printStopRequested(dc.tunnelID);
|
|
949
|
+
const result = await this.tunnelHandler.handleStop(dc.tunnelID);
|
|
950
|
+
remoteManagementWebSocketPrinter.handleStopResult(dc.tunnelID, result);
|
|
951
|
+
return this.wrapResponse(result, req);
|
|
952
|
+
} catch (e) {
|
|
953
|
+
if (e instanceof z2.ZodError) {
|
|
954
|
+
printer_default.warn("Validation failed for stop request");
|
|
955
|
+
return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
|
|
956
|
+
}
|
|
957
|
+
printer_default.warn(`Error in handleStopReq error: ${String(e)}`);
|
|
958
|
+
return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
async handleGetReq(req, raw) {
|
|
962
|
+
try {
|
|
963
|
+
const dc = GetSchema.parse(raw);
|
|
964
|
+
const result = await this.tunnelHandler.handleGet(dc.tunnelID);
|
|
965
|
+
return this.wrapResponse(result, req);
|
|
966
|
+
} catch (e) {
|
|
967
|
+
if (e instanceof z2.ZodError) {
|
|
968
|
+
printer_default.warn("Validation failed for get request");
|
|
969
|
+
return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
|
|
970
|
+
}
|
|
971
|
+
printer_default.warn(`Error in handleGetReq error: ${String(e)}`);
|
|
972
|
+
return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
async handleRestartReq(req, raw) {
|
|
976
|
+
try {
|
|
977
|
+
const dc = RestartSchema.parse(raw);
|
|
978
|
+
remoteManagementWebSocketPrinter.printRestartRequested(dc.tunnelID);
|
|
979
|
+
const result = await this.tunnelHandler.handleRestart(dc.tunnelID, true);
|
|
980
|
+
remoteManagementWebSocketPrinter.handleRestartResult(dc.tunnelID, result);
|
|
981
|
+
return this.wrapResponse(result, req);
|
|
982
|
+
} catch (e) {
|
|
983
|
+
if (e instanceof z2.ZodError) {
|
|
984
|
+
printer_default.warn("Validation failed for restart request");
|
|
985
|
+
return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
|
|
986
|
+
}
|
|
987
|
+
printer_default.warn(`Error in handleRestartReq error: ${String(e)}`);
|
|
988
|
+
return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
async handleUpdateConfigReq(req, raw) {
|
|
992
|
+
try {
|
|
993
|
+
const dc = UpdateConfigSchema.parse(raw);
|
|
994
|
+
const result = await this.tunnelHandler.handleUpdateConfig(dc.tunnelConfig, true);
|
|
995
|
+
return this.wrapResponse(result, req);
|
|
996
|
+
} catch (e) {
|
|
997
|
+
if (e instanceof z2.ZodError) {
|
|
998
|
+
printer_default.warn("Validation failed for updateconfig request");
|
|
999
|
+
return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
|
|
1000
|
+
}
|
|
1001
|
+
printer_default.warn(`Error in handleUpdateConfigReq error: ${String(e)}`);
|
|
1002
|
+
return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
async handleUpdateConfigV2Req(req, raw) {
|
|
1006
|
+
try {
|
|
1007
|
+
const dc = UpdateConfigV2Schema.parse(raw);
|
|
1008
|
+
const result = await this.tunnelHandler.handleUpdateConfigV2(dc.tunnelConfig, true);
|
|
1009
|
+
return this.wrapResponse(result, req);
|
|
1010
|
+
} catch (e) {
|
|
1011
|
+
if (e instanceof z2.ZodError) {
|
|
1012
|
+
printer_default.warn("Validation failed for update-config-v2 request");
|
|
1013
|
+
return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
|
|
1014
|
+
}
|
|
1015
|
+
printer_default.warn(`Error in handleUpdateConfigV2Req error: ${String(e)}`);
|
|
1016
|
+
return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
async handleListReq(req) {
|
|
1020
|
+
try {
|
|
1021
|
+
const result = await this.tunnelHandler.handleList();
|
|
1022
|
+
remoteManagementWebSocketPrinter.monitorList(result);
|
|
1023
|
+
return this.wrapResponse(result, req);
|
|
1024
|
+
} catch (e) {
|
|
1025
|
+
printer_default.warn(`Error in handleListReq error: ${String(e)}`);
|
|
1026
|
+
return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
async handleListV2Req(req) {
|
|
1030
|
+
try {
|
|
1031
|
+
const result = await this.tunnelHandler.handleListV2();
|
|
1032
|
+
remoteManagementWebSocketPrinter.monitorList(result);
|
|
1033
|
+
return this.wrapResponse(result, req);
|
|
1034
|
+
} catch (e) {
|
|
1035
|
+
printer_default.warn(`Error in handleListV2Req error: ${String(e)}`);
|
|
1036
|
+
return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
handleGetVersionReq(ws, req) {
|
|
1040
|
+
try {
|
|
1041
|
+
const versionResponse = {
|
|
1042
|
+
cli_version: getVersion()
|
|
1043
|
+
};
|
|
1044
|
+
const payload = {
|
|
1045
|
+
command: req.command,
|
|
1046
|
+
requestid: req.requestid,
|
|
1047
|
+
response: JSON.stringify(versionResponse),
|
|
1048
|
+
error: false
|
|
1049
|
+
};
|
|
1050
|
+
ws.send(JSON.stringify(payload));
|
|
1051
|
+
} catch (e) {
|
|
1052
|
+
printer_default.warn(`Error in handleGetVersionReq error: ${String(e)}`);
|
|
1053
|
+
this.sendError(ws, req, String(e));
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
wrapResponse(result, req) {
|
|
1057
|
+
if (isErrorResponse(result)) {
|
|
1058
|
+
const errResp = NewErrorResponseObject(result);
|
|
1059
|
+
errResp.command = req.command;
|
|
1060
|
+
errResp.requestid = req.requestid;
|
|
1061
|
+
return errResp;
|
|
1062
|
+
}
|
|
1063
|
+
const finalResult = JSON.parse(JSON.stringify(result));
|
|
1064
|
+
if (Array.isArray(finalResult)) {
|
|
1065
|
+
finalResult.forEach((item) => {
|
|
1066
|
+
if (item?.tunnelconfig) {
|
|
1067
|
+
delete item.tunnelconfig.allowPreflight;
|
|
1068
|
+
}
|
|
1069
|
+
});
|
|
1070
|
+
} else if (finalResult?.tunnelconfig) {
|
|
1071
|
+
delete finalResult.tunnelconfig.allowPreflight;
|
|
1072
|
+
}
|
|
1073
|
+
const respObj = NewResponseObject(finalResult);
|
|
1074
|
+
respObj.command = req.command;
|
|
1075
|
+
respObj.requestid = req.requestid;
|
|
1076
|
+
return respObj;
|
|
1077
|
+
}
|
|
1078
|
+
async handle(ws, req) {
|
|
1079
|
+
const cmd = (req.command || "").toLowerCase();
|
|
1080
|
+
const raw = this.safeParse(req.data);
|
|
1081
|
+
try {
|
|
1082
|
+
let response;
|
|
1083
|
+
switch (cmd) {
|
|
1084
|
+
case WsCommand.Start: {
|
|
1085
|
+
response = await this.handleStartReq(req, raw);
|
|
1086
|
+
break;
|
|
1087
|
+
}
|
|
1088
|
+
case WsCommand.StartV2: {
|
|
1089
|
+
response = await this.handleStartV2Req(req, raw);
|
|
1090
|
+
break;
|
|
1091
|
+
}
|
|
1092
|
+
case WsCommand.Stop: {
|
|
1093
|
+
response = await this.handleStopReq(req, raw);
|
|
1094
|
+
break;
|
|
1095
|
+
}
|
|
1096
|
+
case WsCommand.Get: {
|
|
1097
|
+
response = await this.handleGetReq(req, raw);
|
|
1098
|
+
break;
|
|
1099
|
+
}
|
|
1100
|
+
case WsCommand.Restart: {
|
|
1101
|
+
response = await this.handleRestartReq(req, raw);
|
|
1102
|
+
break;
|
|
1103
|
+
}
|
|
1104
|
+
case WsCommand.UpdateConfig: {
|
|
1105
|
+
response = await this.handleUpdateConfigReq(req, raw);
|
|
1106
|
+
break;
|
|
1107
|
+
}
|
|
1108
|
+
case WsCommand.UpdateConfigV2: {
|
|
1109
|
+
response = await this.handleUpdateConfigV2Req(req, raw);
|
|
1110
|
+
break;
|
|
1111
|
+
}
|
|
1112
|
+
case WsCommand.List: {
|
|
1113
|
+
response = await this.handleListReq(req);
|
|
1114
|
+
break;
|
|
1115
|
+
}
|
|
1116
|
+
case WsCommand.ListV2: {
|
|
1117
|
+
response = await this.handleListV2Req(req);
|
|
1118
|
+
break;
|
|
1119
|
+
}
|
|
1120
|
+
case WsCommand.GetVersion: {
|
|
1121
|
+
this.handleGetVersionReq(ws, req);
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
default:
|
|
1125
|
+
if (typeof req.command === "string") {
|
|
1126
|
+
logger.warn("Unknown command", { command: req.command });
|
|
1127
|
+
}
|
|
1128
|
+
return this.sendError(ws, req, "Invalid command");
|
|
1129
|
+
}
|
|
1130
|
+
logger.debug("Sending response", { command: response.command, requestid: response.requestid });
|
|
1131
|
+
this.sendResponse(ws, response);
|
|
1132
|
+
} catch (e) {
|
|
1133
|
+
if (e instanceof z2.ZodError) {
|
|
1134
|
+
logger.warn("Validation failed", { cmd, issues: e.issues });
|
|
1135
|
+
return this.sendError(ws, req, "Invalid request data", ErrorCode.InvalidBodyFormatError);
|
|
1136
|
+
}
|
|
1137
|
+
logger.error("Error handling command", { cmd, error: errorMessage(e) });
|
|
1138
|
+
return this.sendError(ws, req, errorMessage(e) || "Internal error");
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
};
|
|
1142
|
+
function sendVersionResponse(ws) {
|
|
1143
|
+
const versionResponse = {
|
|
1144
|
+
cli_version: getVersion()
|
|
1145
|
+
};
|
|
1146
|
+
const payload = {
|
|
1147
|
+
command: WsCommand.GetVersion,
|
|
1148
|
+
requestid: "0",
|
|
1149
|
+
response: JSON.stringify(versionResponse),
|
|
1150
|
+
error: false
|
|
1151
|
+
};
|
|
1152
|
+
ws.send(JSON.stringify(payload));
|
|
1153
|
+
}
|
|
1154
|
+
function handleConnectionStatusMessage(firstMessage) {
|
|
1155
|
+
try {
|
|
1156
|
+
const text = typeof firstMessage === "string" ? firstMessage : firstMessage.toString();
|
|
1157
|
+
const cs = JSON.parse(text);
|
|
1158
|
+
if (!cs.success) {
|
|
1159
|
+
const msg = cs.error_msg || "Connection failed";
|
|
1160
|
+
printer_default.warn(`Connection failed: ${msg}`);
|
|
1161
|
+
logger.warn("Remote management connection failed", { error_code: cs.error_code, error_msg: msg });
|
|
1162
|
+
return false;
|
|
1163
|
+
}
|
|
1164
|
+
return true;
|
|
1165
|
+
} catch (e) {
|
|
1166
|
+
logger.warn("Failed to parse connection status message", { error: String(e) });
|
|
1167
|
+
return true;
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// src/remote_management/remoteManagement.ts
|
|
1172
|
+
var RECONNECT_SLEEP_MS = 5e3;
|
|
1173
|
+
var PING_INTERVAL_MS = 3e4;
|
|
1174
|
+
var RemoteManagementUnauthorizedError = class extends Error {
|
|
1175
|
+
constructor() {
|
|
1176
|
+
super("Unauthorized. Please enter a valid token.");
|
|
1177
|
+
this.name = "RemoteManagementUnauthorizedError";
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
var _remoteManagementState = {
|
|
1181
|
+
status: "NOT_RUNNING",
|
|
1182
|
+
errorMessage: ""
|
|
1183
|
+
};
|
|
1184
|
+
var _stopRequested = false;
|
|
1185
|
+
var currentWs = null;
|
|
1186
|
+
function buildRemoteManagementWsUrl(manage) {
|
|
1187
|
+
let baseUrl = (manage || "dashboard.pinggy.io").trim();
|
|
1188
|
+
if (!(baseUrl.startsWith("ws://") || baseUrl.startsWith("wss://"))) {
|
|
1189
|
+
baseUrl = "wss://" + baseUrl;
|
|
1190
|
+
}
|
|
1191
|
+
const trimmed = baseUrl.replace(/\/$/, "");
|
|
1192
|
+
return `${trimmed}/backend/api/v1/remote-management/connect`;
|
|
1193
|
+
}
|
|
1194
|
+
function extractHostname(u) {
|
|
1195
|
+
try {
|
|
1196
|
+
const url = new URL(u);
|
|
1197
|
+
return url.host;
|
|
1198
|
+
} catch {
|
|
1199
|
+
return u;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
function sleep(ms) {
|
|
1203
|
+
return new Promise((res) => setTimeout(res, ms));
|
|
1204
|
+
}
|
|
1205
|
+
async function initiateRemoteManagement(remoteManagementConfig, tunnelHandler) {
|
|
1206
|
+
if (!remoteManagementConfig.apiKey || remoteManagementConfig.apiKey.trim().length === 0) {
|
|
1207
|
+
throw new Error("Remote management token is required (use --remote-management <TOKEN>)");
|
|
1208
|
+
}
|
|
1209
|
+
const wsUrl = remoteManagementConfig.serverUrl;
|
|
1210
|
+
const wsHost = extractHostname(wsUrl);
|
|
1211
|
+
logger.info("Remote management mode enabled.");
|
|
1212
|
+
_stopRequested = false;
|
|
1213
|
+
const sigintHandler = () => {
|
|
1214
|
+
_stopRequested = true;
|
|
1215
|
+
};
|
|
1216
|
+
process.once("SIGINT", sigintHandler);
|
|
1217
|
+
const logConnecting = () => {
|
|
1218
|
+
printer_default.print(`Connecting to ${wsHost}`);
|
|
1219
|
+
logger.info("Connecting to remote management", { wsUrl });
|
|
1220
|
+
};
|
|
1221
|
+
while (!_stopRequested) {
|
|
1222
|
+
logConnecting();
|
|
1223
|
+
setRemoteManagementState({ status: RemoteManagementStatus.Connecting, errorMessage: "" });
|
|
1224
|
+
try {
|
|
1225
|
+
await handleWebSocketConnection(wsUrl, wsHost, remoteManagementConfig.apiKey, void 0, tunnelHandler);
|
|
1226
|
+
} catch (error) {
|
|
1227
|
+
if (error instanceof RemoteManagementUnauthorizedError) {
|
|
1228
|
+
throw error;
|
|
1229
|
+
}
|
|
1230
|
+
logger.warn("Remote management connection error", { error: String(error) });
|
|
1231
|
+
}
|
|
1232
|
+
if (_stopRequested) {
|
|
1233
|
+
break;
|
|
1234
|
+
}
|
|
1235
|
+
printer_default.warn(`Remote management disconnected. Reconnecting in ${RECONNECT_SLEEP_MS / 1e3} seconds...`);
|
|
1236
|
+
logger.info("Reconnecting to remote management after disconnect");
|
|
1237
|
+
await sleep(RECONNECT_SLEEP_MS);
|
|
1238
|
+
}
|
|
1239
|
+
process.removeListener("SIGINT", sigintHandler);
|
|
1240
|
+
logger.info("Remote management stopped.");
|
|
1241
|
+
return getRemoteManagementState();
|
|
1242
|
+
}
|
|
1243
|
+
async function handleWebSocketConnection(wsUrl, wsHost, token, onOpenCallback, tunnelHandler) {
|
|
1244
|
+
return new Promise((resolve, reject) => {
|
|
1245
|
+
const ws = new WebSocket(wsUrl, {
|
|
1246
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1247
|
+
});
|
|
1248
|
+
currentWs = ws;
|
|
1249
|
+
let heartbeat = null;
|
|
1250
|
+
let firstMessage = true;
|
|
1251
|
+
let settled = false;
|
|
1252
|
+
const cleanup = (err) => {
|
|
1253
|
+
if (settled) {
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
settled = true;
|
|
1257
|
+
if (heartbeat) {
|
|
1258
|
+
clearInterval(heartbeat);
|
|
1259
|
+
}
|
|
1260
|
+
currentWs = null;
|
|
1261
|
+
if (err) {
|
|
1262
|
+
reject(err);
|
|
1263
|
+
} else {
|
|
1264
|
+
resolve();
|
|
1265
|
+
}
|
|
1266
|
+
};
|
|
1267
|
+
ws.once("open", () => {
|
|
1268
|
+
printer_default.success(`Connected to ${wsHost}`);
|
|
1269
|
+
setRemoteManagementState({ status: RemoteManagementStatus.Running, errorMessage: "" });
|
|
1270
|
+
onOpenCallback?.();
|
|
1271
|
+
heartbeat = setInterval(() => {
|
|
1272
|
+
if (ws.readyState === WebSocket.OPEN) ws.ping();
|
|
1273
|
+
}, PING_INTERVAL_MS);
|
|
1274
|
+
});
|
|
1275
|
+
ws.on("ping", () => ws.pong());
|
|
1276
|
+
ws.on("message", async (data) => {
|
|
1277
|
+
try {
|
|
1278
|
+
if (firstMessage) {
|
|
1279
|
+
firstMessage = false;
|
|
1280
|
+
const ok = handleConnectionStatusMessage(data);
|
|
1281
|
+
if (!ok) {
|
|
1282
|
+
ws.close();
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
sendVersionResponse(ws);
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
setRemoteManagementState({ status: RemoteManagementStatus.Running, errorMessage: "" });
|
|
1289
|
+
const req = JSON.parse(data.toString("utf8"));
|
|
1290
|
+
await new WebSocketCommandHandler(tunnelHandler).handle(ws, req);
|
|
1291
|
+
} catch (e) {
|
|
1292
|
+
logger.warn("Failed handling websocket message", { error: String(e) });
|
|
1293
|
+
}
|
|
1294
|
+
});
|
|
1295
|
+
ws.on("unexpected-response", (_, res) => {
|
|
1296
|
+
if (res.statusCode === 401) {
|
|
1297
|
+
setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: `HTTP ${res.statusCode}` });
|
|
1298
|
+
logger.error("Unauthorized (401) on remote management connect");
|
|
1299
|
+
cleanup(new RemoteManagementUnauthorizedError());
|
|
1300
|
+
ws.close();
|
|
1301
|
+
} else {
|
|
1302
|
+
logger.warn("Unexpected HTTP response ", { statusCode: res.statusCode });
|
|
1303
|
+
printer_default.warn(`Unexpected HTTP ${res.statusCode}. Retrying...`);
|
|
1304
|
+
cleanup();
|
|
1305
|
+
ws.close();
|
|
1306
|
+
}
|
|
1307
|
+
});
|
|
1308
|
+
ws.on("close", (code, reason) => {
|
|
1309
|
+
setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: "" });
|
|
1310
|
+
logger.info("WebSocket closed", { code, reason: reason.toString() });
|
|
1311
|
+
printer_default.warn(`Disconnected (code: ${code}). Retrying in ${RECONNECT_SLEEP_MS / 1e3}s...`);
|
|
1312
|
+
cleanup();
|
|
1313
|
+
});
|
|
1314
|
+
ws.on("error", (err) => {
|
|
1315
|
+
setRemoteManagementState({ status: RemoteManagementStatus.Error, errorMessage: err.message });
|
|
1316
|
+
logger.warn("WebSocket error", { error: err.message });
|
|
1317
|
+
printer_default.warn(err.message);
|
|
1318
|
+
cleanup();
|
|
1319
|
+
});
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
async function closeRemoteManagement(timeoutMs = 1e4) {
|
|
1323
|
+
_stopRequested = true;
|
|
1324
|
+
try {
|
|
1325
|
+
if (currentWs) {
|
|
1326
|
+
try {
|
|
1327
|
+
setRemoteManagementState({ status: RemoteManagementStatus.Disconnecting, errorMessage: "" });
|
|
1328
|
+
currentWs.close();
|
|
1329
|
+
} catch (e) {
|
|
1330
|
+
logger.warn("Error while closing current remote management websocket", { error: String(e) });
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
const start = Date.now();
|
|
1334
|
+
while (_remoteManagementState.status === "RUNNING") {
|
|
1335
|
+
if (Date.now() - start > timeoutMs) {
|
|
1336
|
+
logger.warn("Timed out waiting for remote management to stop");
|
|
1337
|
+
break;
|
|
1338
|
+
}
|
|
1339
|
+
await sleep(200);
|
|
1340
|
+
}
|
|
1341
|
+
} finally {
|
|
1342
|
+
currentWs = null;
|
|
1343
|
+
setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: "" });
|
|
1344
|
+
return getRemoteManagementState();
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
function startRemoteManagement(remoteManagementConfig, tunnelHandler) {
|
|
1348
|
+
if (!remoteManagementConfig.apiKey || remoteManagementConfig.apiKey.trim().length === 0) {
|
|
1349
|
+
return Promise.reject(new Error("Remote management token is required"));
|
|
1350
|
+
}
|
|
1351
|
+
const wsUrl = remoteManagementConfig.serverUrl;
|
|
1352
|
+
const wsHost = extractHostname(wsUrl);
|
|
1353
|
+
logger.info("Remote management mode enabled.");
|
|
1354
|
+
_stopRequested = false;
|
|
1355
|
+
return new Promise((resolve, reject) => {
|
|
1356
|
+
let firstSettled = false;
|
|
1357
|
+
const settleOnce = (err) => {
|
|
1358
|
+
if (firstSettled) {
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
firstSettled = true;
|
|
1362
|
+
if (err) {
|
|
1363
|
+
reject(err);
|
|
1364
|
+
} else {
|
|
1365
|
+
resolve(getRemoteManagementState());
|
|
1366
|
+
}
|
|
1367
|
+
};
|
|
1368
|
+
const runLoop = async () => {
|
|
1369
|
+
const sigintHandler = () => {
|
|
1370
|
+
_stopRequested = true;
|
|
1371
|
+
};
|
|
1372
|
+
process.once("SIGINT", sigintHandler);
|
|
1373
|
+
while (!_stopRequested) {
|
|
1374
|
+
logger.info("Connecting to remote management", { wsUrl });
|
|
1375
|
+
setRemoteManagementState({ status: RemoteManagementStatus.Connecting, errorMessage: "" });
|
|
1376
|
+
try {
|
|
1377
|
+
await handleWebSocketConnection(wsUrl, wsHost, remoteManagementConfig.apiKey, () => settleOnce(), tunnelHandler);
|
|
1378
|
+
} catch (error) {
|
|
1379
|
+
if (error instanceof RemoteManagementUnauthorizedError) {
|
|
1380
|
+
settleOnce(error);
|
|
1381
|
+
process.removeListener("SIGINT", sigintHandler);
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
settleOnce();
|
|
1385
|
+
logger.warn("Remote management connection error", { error: String(error) });
|
|
1386
|
+
}
|
|
1387
|
+
if (_stopRequested) {
|
|
1388
|
+
break;
|
|
1389
|
+
}
|
|
1390
|
+
logger.info("Reconnecting to remote management after disconnect");
|
|
1391
|
+
await sleep(RECONNECT_SLEEP_MS);
|
|
1392
|
+
}
|
|
1393
|
+
process.removeListener("SIGINT", sigintHandler);
|
|
1394
|
+
logger.info("Remote management stopped.");
|
|
1395
|
+
};
|
|
1396
|
+
runLoop().catch((err) => settleOnce(err instanceof Error ? err : new Error(String(err))));
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
function getRemoteManagementState() {
|
|
1400
|
+
return _remoteManagementState;
|
|
1401
|
+
}
|
|
1402
|
+
function setRemoteManagementState(state, errorMessage2) {
|
|
1403
|
+
_remoteManagementState = {
|
|
1404
|
+
status: state.status,
|
|
1405
|
+
errorMessage: errorMessage2 || ""
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
// src/daemon/lifecycle/daemonManager.ts
|
|
1410
|
+
import os from "os";
|
|
1411
|
+
import fs from "fs";
|
|
1412
|
+
import { spawn } from "child_process";
|
|
1413
|
+
var inProcessHandle = null;
|
|
1414
|
+
var DAEMON_SPAWN_TIMEOUT_MS = 8e3;
|
|
1415
|
+
var DAEMON_POLL_INTERVAL_MS = 200;
|
|
1416
|
+
function isProcessAlive(pid) {
|
|
1417
|
+
try {
|
|
1418
|
+
process.kill(pid, 0);
|
|
1419
|
+
return true;
|
|
1420
|
+
} catch {
|
|
1421
|
+
return false;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
function getDaemonInfo() {
|
|
1425
|
+
const infoPath = getDaemonInfoPath();
|
|
1426
|
+
if (!fs.existsSync(infoPath)) return null;
|
|
1427
|
+
try {
|
|
1428
|
+
const data = JSON.parse(fs.readFileSync(infoPath, "utf-8"));
|
|
1429
|
+
if (!data.pid || !data.port) return null;
|
|
1430
|
+
if (!isProcessAlive(data.pid)) {
|
|
1431
|
+
logger.info("Stale daemon.json found, cleaning up", { stalePid: data.pid });
|
|
1432
|
+
try {
|
|
1433
|
+
fs.unlinkSync(infoPath);
|
|
1434
|
+
} catch {
|
|
1435
|
+
}
|
|
1436
|
+
return null;
|
|
1437
|
+
}
|
|
1438
|
+
return data;
|
|
1439
|
+
} catch {
|
|
1440
|
+
return null;
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
function isDaemonRunning() {
|
|
1444
|
+
return getDaemonInfo() !== null;
|
|
1445
|
+
}
|
|
1446
|
+
function getDaemonSpawnArgs() {
|
|
1447
|
+
return {
|
|
1448
|
+
command: process.execPath,
|
|
1449
|
+
args: [process.argv[1], "--_daemon-child"],
|
|
1450
|
+
env: { ...process.env }
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
async function startDaemon() {
|
|
1454
|
+
const existing = getDaemonInfo();
|
|
1455
|
+
if (existing) {
|
|
1456
|
+
return existing;
|
|
1457
|
+
}
|
|
1458
|
+
const { command, args, env } = getDaemonSpawnArgs();
|
|
1459
|
+
logger.info("Spawning daemon child", { command, args });
|
|
1460
|
+
let stderrOutput = "";
|
|
1461
|
+
let exited = false;
|
|
1462
|
+
let exitCode = null;
|
|
1463
|
+
const child = spawn(command, args, {
|
|
1464
|
+
detached: true,
|
|
1465
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
1466
|
+
env,
|
|
1467
|
+
...os.platform() === "win32" ? { windowsHide: true } : {}
|
|
1468
|
+
});
|
|
1469
|
+
child.stderr?.on("data", (chunk) => {
|
|
1470
|
+
stderrOutput += chunk.toString("utf-8");
|
|
1471
|
+
});
|
|
1472
|
+
child.on("exit", (code) => {
|
|
1473
|
+
exited = true;
|
|
1474
|
+
exitCode = code;
|
|
1475
|
+
});
|
|
1476
|
+
child.unref();
|
|
1477
|
+
const info = await pollForDaemonInfo(DAEMON_SPAWN_TIMEOUT_MS, () => exited);
|
|
1478
|
+
if (!info) {
|
|
1479
|
+
const logPath = getDaemonLogPath();
|
|
1480
|
+
if (exited) {
|
|
1481
|
+
const detail = stderrOutput.trim() || `Check ${logPath} for details.`;
|
|
1482
|
+
throw new Error(`Daemon child exited with code ${exitCode}. ${detail}`);
|
|
1483
|
+
}
|
|
1484
|
+
throw new Error(`Daemon failed to start within timeout. Check ${logPath} for details.`);
|
|
1485
|
+
}
|
|
1486
|
+
child.stderr?.removeAllListeners();
|
|
1487
|
+
child.stderr?.destroy();
|
|
1488
|
+
return info;
|
|
1489
|
+
}
|
|
1490
|
+
async function ensureDaemonRunning() {
|
|
1491
|
+
const existing = getDaemonInfo();
|
|
1492
|
+
if (existing) return existing;
|
|
1493
|
+
if (process.versions.electron) {
|
|
1494
|
+
const { runDaemonChild } = await import("./daemonChild-E2CORSSB.js");
|
|
1495
|
+
const handle = await runDaemonChild({
|
|
1496
|
+
installSignalHandlers: false,
|
|
1497
|
+
exitOnFailure: false
|
|
1498
|
+
});
|
|
1499
|
+
inProcessHandle = handle;
|
|
1500
|
+
return getDaemonInfo() ?? {
|
|
1501
|
+
pid: handle.pid,
|
|
1502
|
+
port: handle.port,
|
|
1503
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
return startDaemon();
|
|
1507
|
+
}
|
|
1508
|
+
function getInProcessDaemonHandle() {
|
|
1509
|
+
return inProcessHandle;
|
|
1510
|
+
}
|
|
1511
|
+
async function getActiveTunnelSummaries(origin = "app") {
|
|
1512
|
+
const info = getDaemonInfo();
|
|
1513
|
+
if (!info) return [];
|
|
1514
|
+
const { IPCClient: IPCClient2 } = await import("./ipcClient-LZQCCNMR.js");
|
|
1515
|
+
const client = new IPCClient2(info.port, origin);
|
|
1516
|
+
let tunnels;
|
|
1517
|
+
try {
|
|
1518
|
+
tunnels = await client.listTunnels();
|
|
1519
|
+
} catch (err) {
|
|
1520
|
+
logger.warn("Failed to list tunnels from daemon", { error: errorMessage(err) });
|
|
1521
|
+
return [];
|
|
1522
|
+
}
|
|
1523
|
+
if (!Array.isArray(tunnels)) return [];
|
|
1524
|
+
return tunnels.filter((t) => {
|
|
1525
|
+
const state = t?.status?.state;
|
|
1526
|
+
return state !== "closed" /* Closed */ && state !== "exited" /* Exited */;
|
|
1527
|
+
}).map((t) => ({
|
|
1528
|
+
tunnelId: t.tunnelid,
|
|
1529
|
+
name: t.tunnelconfig?.name || t.tunnelid.slice(0, 8),
|
|
1530
|
+
localAddress: getLocalAddress(t.tunnelconfig),
|
|
1531
|
+
urls: t.remoteurls ?? []
|
|
1532
|
+
}));
|
|
1533
|
+
}
|
|
1534
|
+
function pollForDaemonInfo(timeoutMs, hasExited) {
|
|
1535
|
+
return new Promise((resolve) => {
|
|
1536
|
+
const start = Date.now();
|
|
1537
|
+
const check = () => {
|
|
1538
|
+
const info = getDaemonInfo();
|
|
1539
|
+
if (info) {
|
|
1540
|
+
resolve(info);
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
if (hasExited?.()) {
|
|
1544
|
+
resolve(null);
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
if (Date.now() - start > timeoutMs) {
|
|
1548
|
+
resolve(null);
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
setTimeout(check, DAEMON_POLL_INTERVAL_MS);
|
|
1552
|
+
};
|
|
1553
|
+
check();
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
async function stopDaemon() {
|
|
1557
|
+
const info = getDaemonInfo();
|
|
1558
|
+
if (!info) return { ok: false, error: "No daemon is running." };
|
|
1559
|
+
let daemonErrors = [];
|
|
1560
|
+
try {
|
|
1561
|
+
const { IPCClient: IPCClient2 } = await import("./ipcClient-LZQCCNMR.js");
|
|
1562
|
+
const client = new IPCClient2(info.port);
|
|
1563
|
+
const result = await client.shutdown();
|
|
1564
|
+
logger.debug("Sent shutdown command to daemon", { result });
|
|
1565
|
+
if (Array.isArray(result?.errors)) daemonErrors = result.errors;
|
|
1566
|
+
} catch (e) {
|
|
1567
|
+
return { ok: false, error: `Failed to reach daemon: ${errorMessage(e)}` };
|
|
1568
|
+
}
|
|
1569
|
+
const exited = await waitForExit(info.pid, 5e3);
|
|
1570
|
+
if (!exited) {
|
|
1571
|
+
const detail = daemonErrors.length > 0 ? ` Daemon reported: ${daemonErrors.join("; ")}` : "";
|
|
1572
|
+
return { ok: false, error: `Daemon PID ${info.pid} did not exit within 5s.${detail}` };
|
|
1573
|
+
}
|
|
1574
|
+
if (daemonErrors.length > 0) {
|
|
1575
|
+
return { ok: false, error: `Daemon exited but reported errors: ${daemonErrors.join("; ")}` };
|
|
1576
|
+
}
|
|
1577
|
+
return { ok: true };
|
|
1578
|
+
}
|
|
1579
|
+
async function waitForExit(pid, timeoutMs) {
|
|
1580
|
+
const deadline = Date.now() + timeoutMs;
|
|
1581
|
+
while (Date.now() < deadline) {
|
|
1582
|
+
if (!isProcessAlive(pid)) return true;
|
|
1583
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1584
|
+
}
|
|
1585
|
+
return !isProcessAlive(pid);
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
// src/daemon/ws/wsStream.ts
|
|
1589
|
+
import { WebSocket as WebSocket2 } from "ws";
|
|
1590
|
+
var WS_NORMAL_CLOSE = 1e3;
|
|
1591
|
+
var WsStream = class {
|
|
1592
|
+
constructor(getWsUrl) {
|
|
1593
|
+
this.getWsUrl = getWsUrl;
|
|
1594
|
+
this.ws = null;
|
|
1595
|
+
this.wsReady = null;
|
|
1596
|
+
this.wsResolve = null;
|
|
1597
|
+
this.subscribedTunnels = /* @__PURE__ */ new Map();
|
|
1598
|
+
this.callbacks = {
|
|
1599
|
+
stats: [],
|
|
1600
|
+
disconnect: [],
|
|
1601
|
+
reconnecting: [],
|
|
1602
|
+
reconnected: [],
|
|
1603
|
+
reconnection_failed: [],
|
|
1604
|
+
error: [],
|
|
1605
|
+
url_ready: [],
|
|
1606
|
+
worker_error: [],
|
|
1607
|
+
will_reconnect: [],
|
|
1608
|
+
stopped: []
|
|
1609
|
+
};
|
|
1610
|
+
this.openListeners = [];
|
|
1611
|
+
this.closeListeners = [];
|
|
1612
|
+
}
|
|
1613
|
+
// Lifecycle
|
|
1614
|
+
async ensureOpen() {
|
|
1615
|
+
if (this.ws && this.ws.readyState === WebSocket2.OPEN) return;
|
|
1616
|
+
this.wsReady = new Promise((resolve) => {
|
|
1617
|
+
this.wsResolve = resolve;
|
|
1618
|
+
});
|
|
1619
|
+
this.ws = new WebSocket2(this.getWsUrl());
|
|
1620
|
+
this.ws.on("open", () => {
|
|
1621
|
+
if (this.wsResolve) {
|
|
1622
|
+
this.wsResolve();
|
|
1623
|
+
this.wsResolve = null;
|
|
1624
|
+
}
|
|
1625
|
+
for (const cb of this.openListeners) {
|
|
1626
|
+
try {
|
|
1627
|
+
cb();
|
|
1628
|
+
} catch (err) {
|
|
1629
|
+
logger.debug("WsStream open listener threw", { error: errorMessage(err) });
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
});
|
|
1633
|
+
this.ws.on("message", (data) => this.handleMessage(data.toString()));
|
|
1634
|
+
this.ws.on("close", (code) => {
|
|
1635
|
+
this.ws = null;
|
|
1636
|
+
this.wsReady = null;
|
|
1637
|
+
this.wsResolve = null;
|
|
1638
|
+
for (const cb of this.closeListeners) {
|
|
1639
|
+
try {
|
|
1640
|
+
cb(code);
|
|
1641
|
+
} catch (err) {
|
|
1642
|
+
logger.debug("WsStream close listener threw", { error: errorMessage(err) });
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
});
|
|
1646
|
+
this.ws.on("error", (err) => {
|
|
1647
|
+
logger.debug("WsStream WS error", { error: errorMessage(err) });
|
|
1648
|
+
});
|
|
1649
|
+
await this.wsReady;
|
|
1650
|
+
}
|
|
1651
|
+
/** Send a normal close (1000). Use for user-initiated shutdown. */
|
|
1652
|
+
closeNormally() {
|
|
1653
|
+
if (!this.ws) return;
|
|
1654
|
+
try {
|
|
1655
|
+
this.ws.close(WS_NORMAL_CLOSE, "Client closing");
|
|
1656
|
+
} catch {
|
|
1657
|
+
}
|
|
1658
|
+
this.ws = null;
|
|
1659
|
+
this.wsReady = null;
|
|
1660
|
+
this.wsResolve = null;
|
|
1661
|
+
this.subscribedTunnels.clear();
|
|
1662
|
+
}
|
|
1663
|
+
/** Hard kill the socket. Use when daemon is known dead. */
|
|
1664
|
+
terminate() {
|
|
1665
|
+
if (!this.ws) return;
|
|
1666
|
+
try {
|
|
1667
|
+
this.ws.terminate();
|
|
1668
|
+
} catch {
|
|
1669
|
+
}
|
|
1670
|
+
this.ws = null;
|
|
1671
|
+
this.wsReady = null;
|
|
1672
|
+
this.wsResolve = null;
|
|
1673
|
+
this.subscribedTunnels.clear();
|
|
1674
|
+
}
|
|
1675
|
+
isOpen() {
|
|
1676
|
+
return !!this.ws && this.ws.readyState === WebSocket2.OPEN;
|
|
1677
|
+
}
|
|
1678
|
+
// Subscriptions
|
|
1679
|
+
async subscribe(tunnelId, mode = SessionMode.Foreground) {
|
|
1680
|
+
await this.ensureOpen();
|
|
1681
|
+
if (this.subscribedTunnels.has(tunnelId)) return;
|
|
1682
|
+
const msg = { type: "subscribe", tunnelId, mode };
|
|
1683
|
+
this.ws.send(JSON.stringify(msg));
|
|
1684
|
+
this.subscribedTunnels.set(tunnelId, { mode });
|
|
1685
|
+
}
|
|
1686
|
+
/**
|
|
1687
|
+
* Remove the local subscription. Sends an unsubscribe frame if the socket
|
|
1688
|
+
* is still open; if not, the daemon cleans up server-side when the session
|
|
1689
|
+
* closes. Returns true if the caller had this subscription.
|
|
1690
|
+
*/
|
|
1691
|
+
unsubscribe(tunnelId) {
|
|
1692
|
+
const wasSubscribed = this.subscribedTunnels.delete(tunnelId);
|
|
1693
|
+
if (!wasSubscribed) return false;
|
|
1694
|
+
if (this.ws && this.ws.readyState === WebSocket2.OPEN) {
|
|
1695
|
+
try {
|
|
1696
|
+
const msg = { type: "unsubscribe", tunnelId };
|
|
1697
|
+
this.ws.send(JSON.stringify(msg));
|
|
1698
|
+
} catch {
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
return true;
|
|
1702
|
+
}
|
|
1703
|
+
hasSubscriptions() {
|
|
1704
|
+
return this.subscribedTunnels.size > 0;
|
|
1705
|
+
}
|
|
1706
|
+
subscriptionCount() {
|
|
1707
|
+
return this.subscribedTunnels.size;
|
|
1708
|
+
}
|
|
1709
|
+
/** Snapshot of current subscriptions, for the reconnect path to replay. */
|
|
1710
|
+
snapshotSubscriptions() {
|
|
1711
|
+
return Array.from(this.subscribedTunnels.entries());
|
|
1712
|
+
}
|
|
1713
|
+
/**
|
|
1714
|
+
* Re-open the WS (fresh socket) and re-subscribe a previously captured set.
|
|
1715
|
+
* Clears existing state first so ensureOpen builds a new connection.
|
|
1716
|
+
*/
|
|
1717
|
+
async restoreSubscriptions(snapshot) {
|
|
1718
|
+
this.subscribedTunnels.clear();
|
|
1719
|
+
this.ws = null;
|
|
1720
|
+
this.wsReady = null;
|
|
1721
|
+
this.wsResolve = null;
|
|
1722
|
+
await this.ensureOpen();
|
|
1723
|
+
for (const [tunnelId, info] of snapshot) {
|
|
1724
|
+
const msg = { type: "subscribe", tunnelId, mode: info.mode };
|
|
1725
|
+
this.ws.send(JSON.stringify(msg));
|
|
1726
|
+
this.subscribedTunnels.set(tunnelId, info);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
// Event registration
|
|
1730
|
+
onOpen(cb) {
|
|
1731
|
+
this.openListeners.push(cb);
|
|
1732
|
+
}
|
|
1733
|
+
onClose(cb) {
|
|
1734
|
+
this.closeListeners.push(cb);
|
|
1735
|
+
}
|
|
1736
|
+
onStats(cb) {
|
|
1737
|
+
this.callbacks.stats.push(cb);
|
|
1738
|
+
}
|
|
1739
|
+
onDisconnect(cb) {
|
|
1740
|
+
this.callbacks.disconnect.push(cb);
|
|
1741
|
+
}
|
|
1742
|
+
onReconnecting(cb) {
|
|
1743
|
+
this.callbacks.reconnecting.push(cb);
|
|
1744
|
+
}
|
|
1745
|
+
onReconnected(cb) {
|
|
1746
|
+
this.callbacks.reconnected.push(cb);
|
|
1747
|
+
}
|
|
1748
|
+
onReconnectionFailed(cb) {
|
|
1749
|
+
this.callbacks.reconnection_failed.push(cb);
|
|
1750
|
+
}
|
|
1751
|
+
onError(cb) {
|
|
1752
|
+
this.callbacks.error.push(cb);
|
|
1753
|
+
}
|
|
1754
|
+
onUrlReady(cb) {
|
|
1755
|
+
this.callbacks.url_ready.push(cb);
|
|
1756
|
+
}
|
|
1757
|
+
onWorkerError(cb) {
|
|
1758
|
+
this.callbacks.worker_error.push(cb);
|
|
1759
|
+
}
|
|
1760
|
+
onWillReconnect(cb) {
|
|
1761
|
+
this.callbacks.will_reconnect.push(cb);
|
|
1762
|
+
}
|
|
1763
|
+
onStopped(cb) {
|
|
1764
|
+
this.callbacks.stopped.push(cb);
|
|
1765
|
+
}
|
|
1766
|
+
// Private
|
|
1767
|
+
handleMessage(raw) {
|
|
1768
|
+
let msg;
|
|
1769
|
+
try {
|
|
1770
|
+
msg = JSON.parse(raw);
|
|
1771
|
+
} catch {
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
if (msg.type !== "tunnel_event") return;
|
|
1775
|
+
const { tunnelId, event, payload } = msg;
|
|
1776
|
+
switch (event) {
|
|
1777
|
+
case "stats":
|
|
1778
|
+
for (const cb of this.callbacks.stats) cb(tunnelId, payload.stats);
|
|
1779
|
+
break;
|
|
1780
|
+
case "disconnect": {
|
|
1781
|
+
const p = payload;
|
|
1782
|
+
for (const cb of this.callbacks.disconnect) cb(tunnelId, p.error, p.messages);
|
|
1783
|
+
break;
|
|
1784
|
+
}
|
|
1785
|
+
case "reconnecting":
|
|
1786
|
+
for (const cb of this.callbacks.reconnecting) cb(tunnelId, payload.retryCnt);
|
|
1787
|
+
break;
|
|
1788
|
+
case "reconnected":
|
|
1789
|
+
for (const cb of this.callbacks.reconnected) cb(tunnelId, payload.urls);
|
|
1790
|
+
break;
|
|
1791
|
+
case "reconnection_failed":
|
|
1792
|
+
for (const cb of this.callbacks.reconnection_failed) cb(tunnelId, payload.retryCnt);
|
|
1793
|
+
break;
|
|
1794
|
+
case "error": {
|
|
1795
|
+
const p = payload;
|
|
1796
|
+
for (const cb of this.callbacks.error) cb(tunnelId, p.message, p.isFatal);
|
|
1797
|
+
break;
|
|
1798
|
+
}
|
|
1799
|
+
case "url_ready":
|
|
1800
|
+
for (const cb of this.callbacks.url_ready) cb(tunnelId, payload.urls);
|
|
1801
|
+
break;
|
|
1802
|
+
case "worker_error":
|
|
1803
|
+
for (const cb of this.callbacks.worker_error) cb(tunnelId, payload.message);
|
|
1804
|
+
break;
|
|
1805
|
+
case "will_reconnect": {
|
|
1806
|
+
const p = payload;
|
|
1807
|
+
for (const cb of this.callbacks.will_reconnect) cb(tunnelId, p.error, p.messages);
|
|
1808
|
+
break;
|
|
1809
|
+
}
|
|
1810
|
+
case "stopped":
|
|
1811
|
+
for (const cb of this.callbacks.stopped) cb(tunnelId);
|
|
1812
|
+
break;
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
};
|
|
1816
|
+
|
|
1817
|
+
// src/daemon/daemonHealth.ts
|
|
1818
|
+
var RECONNECT_ATTEMPTS = 3;
|
|
1819
|
+
var RECONNECT_INTERVAL_MS = 1e3;
|
|
1820
|
+
var HEARTBEAT_INTERVAL_MS = 5e3;
|
|
1821
|
+
var HEARTBEAT_TIMEOUT_MS = 2e3;
|
|
1822
|
+
var HEARTBEAT_FAILURE_THRESHOLD = 2;
|
|
1823
|
+
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1824
|
+
var DaemonHealth = class {
|
|
1825
|
+
constructor(getIpc, stream) {
|
|
1826
|
+
this.getIpc = getIpc;
|
|
1827
|
+
this.stream = stream;
|
|
1828
|
+
this.originalPid = null;
|
|
1829
|
+
this.lost = false;
|
|
1830
|
+
this.reconnecting = false;
|
|
1831
|
+
this.heartbeatTimer = null;
|
|
1832
|
+
this.lostCallbacks = [];
|
|
1833
|
+
this.reconnectingCallbacks = [];
|
|
1834
|
+
this.reconnectedCallbacks = [];
|
|
1835
|
+
this.stream.onOpen(() => this.startHeartbeat());
|
|
1836
|
+
this.stream.onClose((code) => {
|
|
1837
|
+
this.stopHeartbeat();
|
|
1838
|
+
if (code === WS_NORMAL_CLOSE) return;
|
|
1839
|
+
if (this.lost || this.reconnecting) return;
|
|
1840
|
+
if (!this.stream.hasSubscriptions()) return;
|
|
1841
|
+
this.reconnecting = true;
|
|
1842
|
+
this.attemptReconnect().catch((err) => logger.debug("Reconnect loop threw", { error: err?.message })).finally(() => {
|
|
1843
|
+
this.reconnecting = false;
|
|
1844
|
+
});
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
bindPid(pid) {
|
|
1848
|
+
this.originalPid = pid;
|
|
1849
|
+
}
|
|
1850
|
+
isLost() {
|
|
1851
|
+
return this.lost;
|
|
1852
|
+
}
|
|
1853
|
+
onLost(cb) {
|
|
1854
|
+
this.lostCallbacks.push(cb);
|
|
1855
|
+
}
|
|
1856
|
+
onReconnecting(cb) {
|
|
1857
|
+
this.reconnectingCallbacks.push(cb);
|
|
1858
|
+
}
|
|
1859
|
+
onReconnected(cb) {
|
|
1860
|
+
this.reconnectedCallbacks.push(cb);
|
|
1861
|
+
}
|
|
1862
|
+
startHeartbeat() {
|
|
1863
|
+
this.stopHeartbeat();
|
|
1864
|
+
let consecutiveFailures = 0;
|
|
1865
|
+
this.heartbeatTimer = setInterval(async () => {
|
|
1866
|
+
if (this.lost || this.reconnecting) return;
|
|
1867
|
+
const ipc = this.getIpc();
|
|
1868
|
+
if (!ipc) return;
|
|
1869
|
+
try {
|
|
1870
|
+
await ipc.ping(HEARTBEAT_TIMEOUT_MS);
|
|
1871
|
+
consecutiveFailures = 0;
|
|
1872
|
+
} catch (err) {
|
|
1873
|
+
consecutiveFailures += 1;
|
|
1874
|
+
if (consecutiveFailures >= HEARTBEAT_FAILURE_THRESHOLD) {
|
|
1875
|
+
this.triggerLost("heartbeat", errorMessage(err));
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
1879
|
+
}
|
|
1880
|
+
stopHeartbeat() {
|
|
1881
|
+
if (this.heartbeatTimer) {
|
|
1882
|
+
clearInterval(this.heartbeatTimer);
|
|
1883
|
+
this.heartbeatTimer = null;
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
async attemptReconnect() {
|
|
1887
|
+
const snapshot = this.stream.snapshotSubscriptions();
|
|
1888
|
+
for (let attempt = 1; attempt <= RECONNECT_ATTEMPTS; attempt++) {
|
|
1889
|
+
for (const cb of this.reconnectingCallbacks) {
|
|
1890
|
+
try {
|
|
1891
|
+
cb(attempt, RECONNECT_ATTEMPTS);
|
|
1892
|
+
} catch {
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
await sleep2(RECONNECT_INTERVAL_MS);
|
|
1896
|
+
if (this.lost) return;
|
|
1897
|
+
const info = getDaemonInfo();
|
|
1898
|
+
if (!info) {
|
|
1899
|
+
this.triggerLost("dead");
|
|
1900
|
+
return;
|
|
1901
|
+
}
|
|
1902
|
+
if (info.pid !== this.originalPid) {
|
|
1903
|
+
this.triggerLost("respawned", `was pid ${this.originalPid}, now pid ${info.pid}`);
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
try {
|
|
1907
|
+
const ipc = this.getIpc();
|
|
1908
|
+
if (!ipc) throw new Error("IPC client missing");
|
|
1909
|
+
await ipc.ping(HEARTBEAT_TIMEOUT_MS);
|
|
1910
|
+
await this.stream.restoreSubscriptions(snapshot);
|
|
1911
|
+
for (const cb of this.reconnectedCallbacks) {
|
|
1912
|
+
try {
|
|
1913
|
+
cb();
|
|
1914
|
+
} catch {
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
return;
|
|
1918
|
+
} catch (err) {
|
|
1919
|
+
logger.debug("Reconnect attempt failed", { attempt, error: errorMessage(err) });
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
this.triggerLost("hung");
|
|
1923
|
+
}
|
|
1924
|
+
triggerLost(reason, detail) {
|
|
1925
|
+
if (this.lost) return;
|
|
1926
|
+
this.lost = true;
|
|
1927
|
+
this.stopHeartbeat();
|
|
1928
|
+
this.stream.terminate();
|
|
1929
|
+
for (const cb of this.lostCallbacks) {
|
|
1930
|
+
try {
|
|
1931
|
+
cb(reason, detail);
|
|
1932
|
+
} catch (err) {
|
|
1933
|
+
logger.debug("daemon-lost callback threw", { error: errorMessage(err) });
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
};
|
|
1938
|
+
|
|
1939
|
+
// src/daemon/tunnelClient.ts
|
|
1940
|
+
var TunnelClient = class _TunnelClient {
|
|
1941
|
+
constructor(options = {}) {
|
|
1942
|
+
this.ipc = null;
|
|
1943
|
+
this.origin = options.origin ?? "cli";
|
|
1944
|
+
this.stream = new WsStream(() => {
|
|
1945
|
+
if (!this.ipc) throw new Error("TunnelClient not initialized. Call ensureDaemon() first.");
|
|
1946
|
+
return this.ipc.getWsUrl();
|
|
1947
|
+
});
|
|
1948
|
+
this.health = new DaemonHealth(() => this.ipc, this.stream);
|
|
1949
|
+
}
|
|
1950
|
+
async ensureDaemon() {
|
|
1951
|
+
const info = await ensureDaemonRunning();
|
|
1952
|
+
this.ipc = new IPCClient(info.port, this.origin);
|
|
1953
|
+
this.health.bindPid(info.pid);
|
|
1954
|
+
}
|
|
1955
|
+
static async forRemoteManagement() {
|
|
1956
|
+
const client = new _TunnelClient({ origin: "remote" });
|
|
1957
|
+
await client.ensureDaemon();
|
|
1958
|
+
client.health.startHeartbeat();
|
|
1959
|
+
return client;
|
|
1960
|
+
}
|
|
1961
|
+
/**
|
|
1962
|
+
* Close the WebSocket connection and cleanup.
|
|
1963
|
+
*/
|
|
1964
|
+
close() {
|
|
1965
|
+
this.health.stopHeartbeat();
|
|
1966
|
+
this.stream.closeNormally();
|
|
1967
|
+
}
|
|
1968
|
+
async ping() {
|
|
1969
|
+
this.assertClient();
|
|
1970
|
+
return this.ipc.ping();
|
|
1971
|
+
}
|
|
1972
|
+
// Tunnel Operations (HTTP)
|
|
1973
|
+
// Callers construct config from SDK shapes (FinalConfig) that are
|
|
1974
|
+
// structurally compatible with the zod-derived wire type but not nominally
|
|
1975
|
+
// identical. Accept the SDK shape and cast at the IPC boundary.
|
|
1976
|
+
async handleStartV2(config, noWait, mode) {
|
|
1977
|
+
this.assertClient();
|
|
1978
|
+
return this.ipc.startTunnelWithConfig(config, mode ?? SessionMode.Detached, noWait);
|
|
1979
|
+
}
|
|
1980
|
+
async handleStart(config, noWait, mode) {
|
|
1981
|
+
this.assertClient();
|
|
1982
|
+
return this.ipc.startTunnelV1(config, mode ?? SessionMode.Detached, noWait);
|
|
1983
|
+
}
|
|
1984
|
+
async handleUpdateConfig(config, noWait) {
|
|
1985
|
+
this.assertClient();
|
|
1986
|
+
return this.ipc.updateConfig(config, noWait);
|
|
1987
|
+
}
|
|
1988
|
+
async handleUpdateConfigV2(config, noWait) {
|
|
1989
|
+
this.assertClient();
|
|
1990
|
+
return this.ipc.updateConfigV2(config, noWait);
|
|
1991
|
+
}
|
|
1992
|
+
async handleStop(tunnelId) {
|
|
1993
|
+
this.assertClient();
|
|
1994
|
+
return this.ipc.stopTunnel(tunnelId);
|
|
1995
|
+
}
|
|
1996
|
+
async handleListV2() {
|
|
1997
|
+
this.assertClient();
|
|
1998
|
+
return this.ipc.listTunnels();
|
|
1999
|
+
}
|
|
2000
|
+
async handleList() {
|
|
2001
|
+
this.assertClient();
|
|
2002
|
+
return this.ipc.listTunnelsV1();
|
|
2003
|
+
}
|
|
2004
|
+
handleRemoveStoppedTunnelByTunnelId(tunnelId) {
|
|
2005
|
+
this.assertClient();
|
|
2006
|
+
void this.ipc.removeStoppedTunnel({ tunnelid: tunnelId });
|
|
2007
|
+
return true;
|
|
2008
|
+
}
|
|
2009
|
+
handleRemoveStoppedTunnelByConfigId(configId) {
|
|
2010
|
+
this.assertClient();
|
|
2011
|
+
void this.ipc.removeStoppedTunnel({ configId });
|
|
2012
|
+
return true;
|
|
2013
|
+
}
|
|
2014
|
+
async handleGet(tunnelId) {
|
|
2015
|
+
this.assertClient();
|
|
2016
|
+
return this.ipc.getTunnel(tunnelId);
|
|
2017
|
+
}
|
|
2018
|
+
async handleRestart(tunnelId) {
|
|
2019
|
+
this.assertClient();
|
|
2020
|
+
return this.ipc.restartTunnel(tunnelId);
|
|
2021
|
+
}
|
|
2022
|
+
async shutdown() {
|
|
2023
|
+
this.assertClient();
|
|
2024
|
+
await this.ipc.shutdown();
|
|
2025
|
+
this.close();
|
|
2026
|
+
}
|
|
2027
|
+
async getLogLevel() {
|
|
2028
|
+
this.assertClient();
|
|
2029
|
+
const res = await this.ipc.getLogLevel();
|
|
2030
|
+
return res.level;
|
|
2031
|
+
}
|
|
2032
|
+
async setLogLevel(level) {
|
|
2033
|
+
this.assertClient();
|
|
2034
|
+
await this.ipc.setLogLevel(level);
|
|
2035
|
+
}
|
|
2036
|
+
async getTunnelLogging() {
|
|
2037
|
+
this.assertClient();
|
|
2038
|
+
const res = await this.ipc.getTunnelLogging();
|
|
2039
|
+
return res.enabled;
|
|
2040
|
+
}
|
|
2041
|
+
async setTunnelLogging(enabled) {
|
|
2042
|
+
this.assertClient();
|
|
2043
|
+
await this.ipc.setTunnelLogging(enabled);
|
|
2044
|
+
}
|
|
2045
|
+
async getLogPaths() {
|
|
2046
|
+
this.assertClient();
|
|
2047
|
+
return await this.ipc.getLogPaths();
|
|
2048
|
+
}
|
|
2049
|
+
async resolveLogPath(q) {
|
|
2050
|
+
this.assertClient();
|
|
2051
|
+
return await this.ipc.resolveLogPath(q);
|
|
2052
|
+
}
|
|
2053
|
+
async restart(tunnelId) {
|
|
2054
|
+
this.assertClient();
|
|
2055
|
+
await this.ipc.restartTunnel(tunnelId);
|
|
2056
|
+
}
|
|
2057
|
+
// Streaming — delegate to WsStream, guarded by daemon-lost
|
|
2058
|
+
async attach(tunnelId, mode = SessionMode.Foreground) {
|
|
2059
|
+
if (this.health.isLost()) return;
|
|
2060
|
+
await this.stream.subscribe(tunnelId, mode);
|
|
2061
|
+
}
|
|
2062
|
+
detach(tunnelId) {
|
|
2063
|
+
const wasSubscribed = this.stream.unsubscribe(tunnelId);
|
|
2064
|
+
if (!wasSubscribed) return;
|
|
2065
|
+
if (!this.stream.hasSubscriptions()) this.close();
|
|
2066
|
+
}
|
|
2067
|
+
// Event registration — delegate to WsStream
|
|
2068
|
+
onStats(cb) {
|
|
2069
|
+
this.stream.onStats(cb);
|
|
2070
|
+
}
|
|
2071
|
+
onDisconnect(cb) {
|
|
2072
|
+
this.stream.onDisconnect(cb);
|
|
2073
|
+
}
|
|
2074
|
+
onReconnecting(cb) {
|
|
2075
|
+
this.stream.onReconnecting(cb);
|
|
2076
|
+
}
|
|
2077
|
+
onReconnected(cb) {
|
|
2078
|
+
this.stream.onReconnected(cb);
|
|
2079
|
+
}
|
|
2080
|
+
onReconnectionFailed(cb) {
|
|
2081
|
+
this.stream.onReconnectionFailed(cb);
|
|
2082
|
+
}
|
|
2083
|
+
onError(cb) {
|
|
2084
|
+
this.stream.onError(cb);
|
|
2085
|
+
}
|
|
2086
|
+
onUrlReady(cb) {
|
|
2087
|
+
this.stream.onUrlReady(cb);
|
|
2088
|
+
}
|
|
2089
|
+
onWorkerError(cb) {
|
|
2090
|
+
this.stream.onWorkerError(cb);
|
|
2091
|
+
}
|
|
2092
|
+
onWillReconnect(cb) {
|
|
2093
|
+
this.stream.onWillReconnect(cb);
|
|
2094
|
+
}
|
|
2095
|
+
onStopped(cb) {
|
|
2096
|
+
this.stream.onStopped(cb);
|
|
2097
|
+
}
|
|
2098
|
+
// Daemon-loss events — delegate to DaemonHealth
|
|
2099
|
+
onDaemonLost(cb) {
|
|
2100
|
+
this.health.onLost(cb);
|
|
2101
|
+
}
|
|
2102
|
+
onDaemonReconnecting(cb) {
|
|
2103
|
+
this.health.onReconnecting(cb);
|
|
2104
|
+
}
|
|
2105
|
+
onDaemonReconnected(cb) {
|
|
2106
|
+
this.health.onReconnected(cb);
|
|
2107
|
+
}
|
|
2108
|
+
isDaemonLost() {
|
|
2109
|
+
return this.health.isLost();
|
|
2110
|
+
}
|
|
2111
|
+
// App-compat shims (register listener + auto-attach in detached mode)
|
|
2112
|
+
handleRegisterStatsListener(tunnelId, listener) {
|
|
2113
|
+
this.onStats(listener);
|
|
2114
|
+
this.attach(tunnelId, "detached").catch(() => {
|
|
2115
|
+
});
|
|
2116
|
+
}
|
|
2117
|
+
handleRegisterDisconnectListener(tunnelId, listener) {
|
|
2118
|
+
this.onDisconnect(listener);
|
|
2119
|
+
this.attach(tunnelId, "detached").catch(() => {
|
|
2120
|
+
});
|
|
2121
|
+
}
|
|
2122
|
+
handleUnregisterStatsListener(_tunnelId, _listenerId) {
|
|
2123
|
+
}
|
|
2124
|
+
async handleGetTunnelStats(tunnelId) {
|
|
2125
|
+
this.assertClient();
|
|
2126
|
+
return this.ipc.getTunnelStats(tunnelId);
|
|
2127
|
+
}
|
|
2128
|
+
// Private
|
|
2129
|
+
assertClient() {
|
|
2130
|
+
if (!this.ipc) {
|
|
2131
|
+
throw new Error("TunnelClient not initialized. Call ensureDaemon() first.");
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
};
|
|
2135
|
+
|
|
2136
|
+
export {
|
|
2137
|
+
TunnelStateType,
|
|
2138
|
+
TunnelErrorCodeType,
|
|
2139
|
+
TunnelWarningCode,
|
|
2140
|
+
ErrorCode,
|
|
2141
|
+
isErrorResponse,
|
|
2142
|
+
TunnelOperations,
|
|
2143
|
+
RemoteManagementUnauthorizedError,
|
|
2144
|
+
buildRemoteManagementWsUrl,
|
|
2145
|
+
initiateRemoteManagement,
|
|
2146
|
+
closeRemoteManagement,
|
|
2147
|
+
startRemoteManagement,
|
|
2148
|
+
getRemoteManagementState,
|
|
2149
|
+
getDaemonInfo,
|
|
2150
|
+
isDaemonRunning,
|
|
2151
|
+
startDaemon,
|
|
2152
|
+
ensureDaemonRunning,
|
|
2153
|
+
getInProcessDaemonHandle,
|
|
2154
|
+
getActiveTunnelSummaries,
|
|
2155
|
+
stopDaemon,
|
|
2156
|
+
TunnelClient
|
|
2157
|
+
};
|