@dainprotocol/tunnel 1.1.25 → 1.1.29
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/dist/client/index.d.ts +5 -7
- package/dist/client/index.js +239 -186
- package/dist/server/index.d.ts +7 -1
- package/dist/server/index.js +365 -331
- package/dist/server/start.js +19 -0
- package/package.json +3 -2
package/dist/client/index.d.ts
CHANGED
|
@@ -5,22 +5,21 @@ declare class DainTunnel extends EventEmitter {
|
|
|
5
5
|
private tunnelUrl;
|
|
6
6
|
private port;
|
|
7
7
|
private reconnectAttempts;
|
|
8
|
-
private maxReconnectAttempts;
|
|
9
|
-
private reconnectDelay;
|
|
10
8
|
private apiKey;
|
|
11
9
|
private tunnelId;
|
|
12
10
|
private secret;
|
|
13
11
|
private webSocketClients;
|
|
14
12
|
private sseClients;
|
|
15
13
|
private httpAgent;
|
|
14
|
+
private heartbeatInterval;
|
|
16
15
|
constructor(serverUrl: string, apiKey: string);
|
|
17
|
-
/**
|
|
18
|
-
* Sign a challenge using HMAC-SHA256
|
|
19
|
-
* @private
|
|
20
|
-
*/
|
|
21
16
|
private signChallenge;
|
|
17
|
+
private safeSend;
|
|
18
|
+
private startHeartbeat;
|
|
19
|
+
private stopHeartbeat;
|
|
22
20
|
start(port: number): Promise<string>;
|
|
23
21
|
private connect;
|
|
22
|
+
private cleanupAllClients;
|
|
24
23
|
private requestChallenge;
|
|
25
24
|
private handleMessage;
|
|
26
25
|
private handleRequest;
|
|
@@ -29,7 +28,6 @@ declare class DainTunnel extends EventEmitter {
|
|
|
29
28
|
private handleSSEConnection;
|
|
30
29
|
private handleSSEClose;
|
|
31
30
|
private forwardRequest;
|
|
32
|
-
private sendMessage;
|
|
33
31
|
private attemptReconnect;
|
|
34
32
|
stop(): Promise<void>;
|
|
35
33
|
}
|
package/dist/client/index.js
CHANGED
|
@@ -9,6 +9,17 @@ const http_1 = __importDefault(require("http"));
|
|
|
9
9
|
const events_1 = require("events");
|
|
10
10
|
const crypto_1 = require("crypto");
|
|
11
11
|
const auth_1 = require("@dainprotocol/service-sdk/service/auth");
|
|
12
|
+
const TIMEOUTS = {
|
|
13
|
+
HEARTBEAT: 25000,
|
|
14
|
+
REQUEST: 25000,
|
|
15
|
+
CONNECTION: 10000,
|
|
16
|
+
CHALLENGE: 5000,
|
|
17
|
+
RECONNECT_DELAY: 5000,
|
|
18
|
+
SHUTDOWN_GRACE: 500,
|
|
19
|
+
};
|
|
20
|
+
const RECONNECT = {
|
|
21
|
+
MAX_ATTEMPTS: 5,
|
|
22
|
+
};
|
|
12
23
|
class DainTunnel extends events_1.EventEmitter {
|
|
13
24
|
constructor(serverUrl, apiKey) {
|
|
14
25
|
super();
|
|
@@ -17,34 +28,58 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
17
28
|
this.tunnelUrl = null;
|
|
18
29
|
this.port = null;
|
|
19
30
|
this.reconnectAttempts = 0;
|
|
20
|
-
this.maxReconnectAttempts = 5;
|
|
21
|
-
this.reconnectDelay = 5000;
|
|
22
31
|
this.webSocketClients = new Map();
|
|
23
32
|
this.sseClients = new Map();
|
|
24
|
-
|
|
33
|
+
this.heartbeatInterval = null;
|
|
25
34
|
const parsed = (0, auth_1.parseAPIKey)(apiKey);
|
|
26
35
|
if (!parsed) {
|
|
27
36
|
throw new Error('Invalid API key format. Expected: sk_agent_{agentId}_{orgId}_{secret}');
|
|
28
37
|
}
|
|
29
38
|
this.apiKey = apiKey;
|
|
30
|
-
this.tunnelId = `${parsed.orgId}_${parsed.agentId}`;
|
|
31
|
-
this.secret = parsed.secret;
|
|
32
|
-
// High-frequency optimization: Create reusable HTTP agent with connection pooling
|
|
39
|
+
this.tunnelId = `${parsed.orgId}_${parsed.agentId}`;
|
|
40
|
+
this.secret = parsed.secret;
|
|
33
41
|
this.httpAgent = new http_1.default.Agent({
|
|
34
42
|
keepAlive: true,
|
|
35
|
-
keepAliveMsecs: 30000,
|
|
36
|
-
maxSockets:
|
|
37
|
-
maxFreeSockets:
|
|
43
|
+
keepAliveMsecs: 30000,
|
|
44
|
+
maxSockets: 100,
|
|
45
|
+
maxFreeSockets: 20,
|
|
38
46
|
});
|
|
39
47
|
}
|
|
40
|
-
/**
|
|
41
|
-
* Sign a challenge using HMAC-SHA256
|
|
42
|
-
* @private
|
|
43
|
-
*/
|
|
44
48
|
signChallenge(challenge) {
|
|
45
|
-
return (0, crypto_1.createHmac)('sha256', this.secret)
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
return (0, crypto_1.createHmac)('sha256', this.secret).update(challenge).digest('hex');
|
|
50
|
+
}
|
|
51
|
+
safeSend(data) {
|
|
52
|
+
var _a;
|
|
53
|
+
try {
|
|
54
|
+
if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
|
|
55
|
+
this.ws.send(JSON.stringify(data));
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (_b) {
|
|
60
|
+
// Connection lost during send
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
startHeartbeat() {
|
|
65
|
+
this.stopHeartbeat();
|
|
66
|
+
this.heartbeatInterval = setInterval(() => {
|
|
67
|
+
var _a;
|
|
68
|
+
if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
|
|
69
|
+
try {
|
|
70
|
+
this.ws.ping();
|
|
71
|
+
}
|
|
72
|
+
catch (_b) {
|
|
73
|
+
this.emit("error", new Error("Heartbeat ping failed"));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}, TIMEOUTS.HEARTBEAT);
|
|
77
|
+
}
|
|
78
|
+
stopHeartbeat() {
|
|
79
|
+
if (this.heartbeatInterval) {
|
|
80
|
+
clearInterval(this.heartbeatInterval);
|
|
81
|
+
this.heartbeatInterval = null;
|
|
82
|
+
}
|
|
48
83
|
}
|
|
49
84
|
async start(port) {
|
|
50
85
|
this.port = port;
|
|
@@ -52,6 +87,23 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
52
87
|
}
|
|
53
88
|
async connect() {
|
|
54
89
|
return new Promise((resolve, reject) => {
|
|
90
|
+
let resolved = false;
|
|
91
|
+
let connectionTimeoutId = null;
|
|
92
|
+
const finish = (value, error) => {
|
|
93
|
+
if (resolved)
|
|
94
|
+
return;
|
|
95
|
+
resolved = true;
|
|
96
|
+
if (connectionTimeoutId) {
|
|
97
|
+
clearTimeout(connectionTimeoutId);
|
|
98
|
+
connectionTimeoutId = null;
|
|
99
|
+
}
|
|
100
|
+
if (error) {
|
|
101
|
+
reject(error);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
resolve(value);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
55
107
|
try {
|
|
56
108
|
this.ws = new ws_1.default(this.serverUrl);
|
|
57
109
|
this.ws.on("open", async () => {
|
|
@@ -59,90 +111,113 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
59
111
|
try {
|
|
60
112
|
const challenge = await this.requestChallenge();
|
|
61
113
|
const signature = this.signChallenge(challenge);
|
|
62
|
-
this.
|
|
114
|
+
this.safeSend({
|
|
63
115
|
type: "start",
|
|
64
116
|
port: this.port,
|
|
65
117
|
challenge,
|
|
66
118
|
signature,
|
|
67
119
|
tunnelId: this.tunnelId,
|
|
68
|
-
apiKey: this.apiKey
|
|
120
|
+
apiKey: this.apiKey
|
|
69
121
|
});
|
|
122
|
+
this.startHeartbeat();
|
|
70
123
|
this.emit("connected");
|
|
71
124
|
}
|
|
72
125
|
catch (err) {
|
|
73
|
-
|
|
126
|
+
finish(undefined, err);
|
|
74
127
|
}
|
|
75
128
|
});
|
|
76
129
|
this.ws.on("message", (data) => {
|
|
77
130
|
try {
|
|
78
|
-
this.handleMessage(JSON.parse(data),
|
|
131
|
+
this.handleMessage(JSON.parse(data), (url) => finish(url));
|
|
79
132
|
}
|
|
80
133
|
catch (err) {
|
|
81
|
-
|
|
134
|
+
finish(undefined, err);
|
|
82
135
|
}
|
|
83
136
|
});
|
|
84
137
|
this.ws.on("close", () => {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
try {
|
|
88
|
-
client.destroy();
|
|
89
|
-
}
|
|
90
|
-
catch (e) { /* ignore */ }
|
|
91
|
-
}
|
|
92
|
-
this.sseClients.clear();
|
|
93
|
-
// Clean up all WebSocket clients
|
|
94
|
-
for (const [, client] of this.webSocketClients) {
|
|
95
|
-
try {
|
|
96
|
-
if (client.readyState === ws_1.default.OPEN)
|
|
97
|
-
client.close(1001);
|
|
98
|
-
}
|
|
99
|
-
catch (e) { /* ignore */ }
|
|
100
|
-
}
|
|
101
|
-
this.webSocketClients.clear();
|
|
138
|
+
this.stopHeartbeat();
|
|
139
|
+
this.cleanupAllClients();
|
|
102
140
|
if (this.tunnelUrl) {
|
|
103
141
|
this.emit("disconnected");
|
|
104
142
|
this.attemptReconnect();
|
|
105
143
|
}
|
|
106
144
|
else {
|
|
107
|
-
|
|
145
|
+
finish(undefined, new Error("Connection closed before tunnel established"));
|
|
108
146
|
}
|
|
109
147
|
});
|
|
110
148
|
this.ws.on("error", (error) => this.emit("error", error));
|
|
111
|
-
setTimeout(() => {
|
|
112
|
-
|
|
113
|
-
|
|
149
|
+
connectionTimeoutId = setTimeout(() => {
|
|
150
|
+
var _a;
|
|
151
|
+
if (!resolved && (!this.ws || this.ws.readyState !== ws_1.default.OPEN)) {
|
|
152
|
+
finish(undefined, new Error("Connection timeout"));
|
|
153
|
+
try {
|
|
154
|
+
(_a = this.ws) === null || _a === void 0 ? void 0 : _a.terminate();
|
|
155
|
+
}
|
|
156
|
+
catch (_b) { }
|
|
114
157
|
}
|
|
115
|
-
},
|
|
158
|
+
}, TIMEOUTS.CONNECTION);
|
|
116
159
|
}
|
|
117
160
|
catch (err) {
|
|
118
|
-
|
|
161
|
+
finish(undefined, err);
|
|
119
162
|
}
|
|
120
163
|
});
|
|
121
164
|
}
|
|
165
|
+
cleanupAllClients() {
|
|
166
|
+
for (const client of this.sseClients.values()) {
|
|
167
|
+
try {
|
|
168
|
+
client.destroy();
|
|
169
|
+
}
|
|
170
|
+
catch (_a) { }
|
|
171
|
+
}
|
|
172
|
+
this.sseClients.clear();
|
|
173
|
+
for (const client of this.webSocketClients.values()) {
|
|
174
|
+
try {
|
|
175
|
+
if (client.readyState === ws_1.default.OPEN)
|
|
176
|
+
client.close(1001);
|
|
177
|
+
}
|
|
178
|
+
catch (_b) { }
|
|
179
|
+
}
|
|
180
|
+
this.webSocketClients.clear();
|
|
181
|
+
}
|
|
122
182
|
async requestChallenge() {
|
|
123
183
|
return new Promise((resolve, reject) => {
|
|
124
184
|
if (!this.ws) {
|
|
125
185
|
reject(new Error("WebSocket is not connected"));
|
|
126
186
|
return;
|
|
127
187
|
}
|
|
128
|
-
|
|
188
|
+
let resolved = false;
|
|
189
|
+
let timeoutId = null;
|
|
190
|
+
const finish = (challenge, error) => {
|
|
191
|
+
var _a;
|
|
192
|
+
if (resolved)
|
|
193
|
+
return;
|
|
194
|
+
resolved = true;
|
|
195
|
+
(_a = this.ws) === null || _a === void 0 ? void 0 : _a.removeListener("message", challengeHandler);
|
|
196
|
+
if (timeoutId) {
|
|
197
|
+
clearTimeout(timeoutId);
|
|
198
|
+
timeoutId = null;
|
|
199
|
+
}
|
|
200
|
+
if (error) {
|
|
201
|
+
reject(error);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
resolve(challenge);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
129
207
|
const challengeHandler = (message) => {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (
|
|
133
|
-
|
|
208
|
+
try {
|
|
209
|
+
const data = JSON.parse(message);
|
|
210
|
+
if (data.type === "challenge") {
|
|
211
|
+
finish(data.challenge);
|
|
134
212
|
}
|
|
135
|
-
resolve(data.challenge);
|
|
136
213
|
}
|
|
214
|
+
catch (_a) { }
|
|
137
215
|
};
|
|
138
216
|
this.ws.on("message", challengeHandler);
|
|
139
|
-
|
|
140
|
-
setTimeout(() => {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
reject(new Error("Challenge request timeout"));
|
|
145
|
-
}, 5000);
|
|
217
|
+
this.ws.send(JSON.stringify({ type: "challenge_request" }));
|
|
218
|
+
timeoutId = setTimeout(() => {
|
|
219
|
+
finish(undefined, new Error("Challenge request timeout"));
|
|
220
|
+
}, TIMEOUTS.CHALLENGE);
|
|
146
221
|
});
|
|
147
222
|
}
|
|
148
223
|
handleMessage(message, resolve) {
|
|
@@ -172,120 +247,119 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
172
247
|
async handleRequest(request) {
|
|
173
248
|
try {
|
|
174
249
|
const response = await this.forwardRequest(request);
|
|
175
|
-
this.
|
|
250
|
+
this.safeSend(response);
|
|
176
251
|
this.emit("request_handled", { request, response });
|
|
177
252
|
}
|
|
178
253
|
catch (error) {
|
|
254
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
255
|
+
this.safeSend({
|
|
256
|
+
type: "response",
|
|
257
|
+
requestId: request.id,
|
|
258
|
+
status: 502,
|
|
259
|
+
headers: { "content-type": "application/json" },
|
|
260
|
+
body: Buffer.from(JSON.stringify({
|
|
261
|
+
error: "Bad Gateway",
|
|
262
|
+
message: `Failed to forward request to local service: ${errorMessage}`
|
|
263
|
+
})).toString("base64")
|
|
264
|
+
});
|
|
179
265
|
this.emit("request_error", { request, error });
|
|
180
266
|
}
|
|
181
267
|
}
|
|
182
268
|
handleWebSocketConnection(message) {
|
|
183
|
-
|
|
269
|
+
const sendWsEvent = (event, data) => {
|
|
270
|
+
this.safeSend({ type: 'websocket', id: message.id, event, data });
|
|
271
|
+
};
|
|
184
272
|
try {
|
|
185
273
|
const client = new ws_1.default(`ws://localhost:${this.port}${message.path}`, {
|
|
186
274
|
headers: message.headers
|
|
187
275
|
});
|
|
188
276
|
this.webSocketClients.set(message.id, client);
|
|
189
277
|
client.on('message', (data) => {
|
|
190
|
-
|
|
191
|
-
if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
|
|
192
|
-
this.ws.send(JSON.stringify({ type: 'websocket', id: message.id, event: 'message', data: data.toString('base64') }));
|
|
193
|
-
}
|
|
278
|
+
sendWsEvent('message', data.toString('base64'));
|
|
194
279
|
});
|
|
195
280
|
client.on('close', () => {
|
|
196
|
-
|
|
197
|
-
if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
|
|
198
|
-
this.ws.send(JSON.stringify({ type: 'websocket', id: message.id, event: 'close' }));
|
|
199
|
-
}
|
|
281
|
+
sendWsEvent('close');
|
|
200
282
|
this.webSocketClients.delete(message.id);
|
|
201
283
|
});
|
|
202
284
|
client.on('error', (error) => {
|
|
203
|
-
|
|
204
|
-
if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
|
|
205
|
-
this.ws.send(JSON.stringify({ type: 'websocket', id: message.id, event: 'error', data: error.message }));
|
|
206
|
-
}
|
|
285
|
+
sendWsEvent('error', error.message);
|
|
207
286
|
this.webSocketClients.delete(message.id);
|
|
208
287
|
});
|
|
209
288
|
this.emit("websocket_connection", { id: message.id, path: message.path });
|
|
210
289
|
}
|
|
211
290
|
catch (error) {
|
|
212
|
-
|
|
213
|
-
this.ws.send(JSON.stringify({ type: 'websocket', id: message.id, event: 'error', data: error.message }));
|
|
214
|
-
}
|
|
291
|
+
sendWsEvent('error', error.message);
|
|
215
292
|
}
|
|
216
293
|
}
|
|
217
294
|
handleWebSocketMessage(message) {
|
|
218
295
|
const client = this.webSocketClients.get(message.id);
|
|
219
296
|
if (!client)
|
|
220
297
|
return;
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
298
|
+
switch (message.event) {
|
|
299
|
+
case 'message':
|
|
300
|
+
if (message.data && client.readyState === ws_1.default.OPEN) {
|
|
301
|
+
client.send(Buffer.from(message.data, 'base64'));
|
|
302
|
+
}
|
|
303
|
+
break;
|
|
304
|
+
case 'close':
|
|
305
|
+
if (client.readyState === ws_1.default.OPEN) {
|
|
306
|
+
client.close();
|
|
307
|
+
}
|
|
308
|
+
this.webSocketClients.delete(message.id);
|
|
309
|
+
break;
|
|
232
310
|
}
|
|
233
311
|
}
|
|
234
312
|
handleSSEConnection(message) {
|
|
235
|
-
|
|
313
|
+
const sendSseEvent = (event, data = '') => {
|
|
314
|
+
this.safeSend({ type: 'sse', id: message.id, event, data });
|
|
315
|
+
};
|
|
236
316
|
try {
|
|
317
|
+
const headers = { ...message.headers };
|
|
318
|
+
delete headers.host;
|
|
319
|
+
headers['accept-encoding'] = 'identity';
|
|
237
320
|
const req = http_1.default.request({
|
|
238
321
|
hostname: 'localhost',
|
|
239
322
|
port: this.port,
|
|
240
323
|
path: message.path,
|
|
241
324
|
method: message.method || 'GET',
|
|
242
|
-
headers
|
|
325
|
+
headers,
|
|
243
326
|
agent: this.httpAgent,
|
|
244
327
|
}, (res) => {
|
|
245
|
-
var _a;
|
|
246
|
-
// Non-200 response - forward error to tunnel server
|
|
247
328
|
if (res.statusCode !== 200) {
|
|
248
329
|
let errorBody = '';
|
|
249
330
|
res.on('data', (chunk) => { errorBody += chunk.toString(); });
|
|
250
331
|
res.on('end', () => {
|
|
251
|
-
|
|
252
|
-
if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
|
|
253
|
-
this.ws.send(JSON.stringify({
|
|
254
|
-
type: 'sse', id: message.id, event: 'error',
|
|
255
|
-
data: `Status ${res.statusCode}: ${errorBody.substring(0, 200)}`
|
|
256
|
-
}));
|
|
257
|
-
}
|
|
332
|
+
sendSseEvent('error', `Status ${res.statusCode}: ${errorBody.substring(0, 200)}`);
|
|
258
333
|
});
|
|
259
334
|
return;
|
|
260
335
|
}
|
|
261
|
-
// Optimize TCP for streaming
|
|
262
336
|
const socket = res.socket || res.connection;
|
|
263
337
|
if (socket === null || socket === void 0 ? void 0 : socket.setNoDelay)
|
|
264
338
|
socket.setNoDelay(true);
|
|
265
|
-
|
|
266
|
-
if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
|
|
267
|
-
this.ws.send(JSON.stringify({ type: 'sse', id: message.id, event: 'connected', data: '' }));
|
|
268
|
-
}
|
|
269
|
-
// Stream SSE events to tunnel server
|
|
339
|
+
sendSseEvent('connected');
|
|
270
340
|
let buffer = '';
|
|
271
|
-
// Helper to process SSE events from buffer
|
|
272
341
|
const processBuffer = () => {
|
|
273
|
-
|
|
342
|
+
buffer = buffer.replace(/\r\n/g, '\n');
|
|
274
343
|
while (buffer.includes('\n\n')) {
|
|
275
344
|
const idx = buffer.indexOf('\n\n');
|
|
276
345
|
const msgData = buffer.substring(0, idx);
|
|
277
346
|
buffer = buffer.substring(idx + 2);
|
|
278
|
-
|
|
347
|
+
if (!msgData.trim() || msgData.startsWith(':'))
|
|
348
|
+
continue;
|
|
349
|
+
let event = 'message';
|
|
350
|
+
let data = '';
|
|
279
351
|
for (const line of msgData.split('\n')) {
|
|
280
|
-
if (line.startsWith('event:'))
|
|
352
|
+
if (line.startsWith('event:')) {
|
|
281
353
|
event = line.substring(6).trim();
|
|
282
|
-
|
|
354
|
+
}
|
|
355
|
+
else if (line.startsWith('data:')) {
|
|
283
356
|
data += line.substring(5).trim() + '\n';
|
|
357
|
+
}
|
|
284
358
|
}
|
|
285
359
|
if (data.endsWith('\n'))
|
|
286
360
|
data = data.slice(0, -1);
|
|
287
|
-
if (
|
|
288
|
-
|
|
361
|
+
if (data) {
|
|
362
|
+
sendSseEvent(event, data);
|
|
289
363
|
}
|
|
290
364
|
}
|
|
291
365
|
};
|
|
@@ -294,34 +368,29 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
294
368
|
processBuffer();
|
|
295
369
|
});
|
|
296
370
|
res.on('end', () => {
|
|
297
|
-
var _a;
|
|
298
|
-
// Process any remaining data in buffer (might be missing final \n\n)
|
|
299
371
|
if (buffer.trim()) {
|
|
300
|
-
// Add missing newlines to ensure processing
|
|
301
372
|
buffer += '\n\n';
|
|
302
373
|
processBuffer();
|
|
303
374
|
}
|
|
304
|
-
|
|
305
|
-
this.ws.send(JSON.stringify({ type: 'sse', id: message.id, event: 'close', data: '' }));
|
|
306
|
-
}
|
|
375
|
+
sendSseEvent('close');
|
|
307
376
|
});
|
|
308
377
|
});
|
|
378
|
+
req.on('socket', (socket) => {
|
|
379
|
+
socket.setNoDelay(true);
|
|
380
|
+
socket.setTimeout(0);
|
|
381
|
+
});
|
|
309
382
|
req.on('error', (error) => {
|
|
310
|
-
|
|
311
|
-
if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
|
|
312
|
-
this.ws.send(JSON.stringify({ type: 'sse', id: message.id, event: 'error', data: error.message }));
|
|
313
|
-
}
|
|
383
|
+
sendSseEvent('error', error.message);
|
|
314
384
|
});
|
|
315
|
-
if (message.body && message.method !== 'GET')
|
|
385
|
+
if (message.body && message.method !== 'GET') {
|
|
316
386
|
req.write(Buffer.from(message.body, 'base64'));
|
|
387
|
+
}
|
|
317
388
|
req.end();
|
|
318
389
|
this.sseClients.set(message.id, req);
|
|
319
390
|
this.emit("sse_connection", { id: message.id, path: message.path });
|
|
320
391
|
}
|
|
321
392
|
catch (error) {
|
|
322
|
-
|
|
323
|
-
this.ws.send(JSON.stringify({ type: 'sse', id: message.id, event: 'error', data: error.message }));
|
|
324
|
-
}
|
|
393
|
+
sendSseEvent('error', error.message);
|
|
325
394
|
}
|
|
326
395
|
}
|
|
327
396
|
handleSSEClose(message) {
|
|
@@ -333,108 +402,92 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
333
402
|
}
|
|
334
403
|
forwardRequest(request) {
|
|
335
404
|
return new Promise((resolve, reject) => {
|
|
405
|
+
let resolved = false;
|
|
406
|
+
let timeoutId = null;
|
|
407
|
+
const finish = (response, error) => {
|
|
408
|
+
if (resolved)
|
|
409
|
+
return;
|
|
410
|
+
resolved = true;
|
|
411
|
+
if (timeoutId) {
|
|
412
|
+
clearTimeout(timeoutId);
|
|
413
|
+
timeoutId = null;
|
|
414
|
+
}
|
|
415
|
+
if (error) {
|
|
416
|
+
reject(error);
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
resolve(response);
|
|
420
|
+
}
|
|
421
|
+
};
|
|
336
422
|
const options = {
|
|
337
423
|
hostname: 'localhost',
|
|
338
424
|
port: this.port,
|
|
339
425
|
path: request.path,
|
|
340
426
|
method: request.method,
|
|
341
427
|
headers: request.headers,
|
|
342
|
-
agent: this.httpAgent,
|
|
428
|
+
agent: this.httpAgent,
|
|
429
|
+
timeout: TIMEOUTS.REQUEST,
|
|
343
430
|
};
|
|
344
431
|
const req = http_1.default.request(options, (res) => {
|
|
345
|
-
|
|
346
|
-
res.on('data', (chunk) =>
|
|
347
|
-
body = Buffer.concat([body, chunk]);
|
|
348
|
-
});
|
|
432
|
+
const chunks = [];
|
|
433
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
349
434
|
res.on('end', () => {
|
|
350
435
|
const headers = { ...res.headers };
|
|
351
436
|
delete headers['transfer-encoding'];
|
|
352
437
|
delete headers['content-length'];
|
|
353
|
-
|
|
438
|
+
finish({
|
|
354
439
|
type: 'response',
|
|
355
440
|
requestId: request.id,
|
|
356
441
|
status: res.statusCode,
|
|
357
442
|
headers,
|
|
358
|
-
body:
|
|
443
|
+
body: Buffer.concat(chunks).toString('base64'),
|
|
359
444
|
});
|
|
360
445
|
});
|
|
446
|
+
res.on('error', (err) => finish(undefined, err));
|
|
447
|
+
});
|
|
448
|
+
req.on('timeout', () => {
|
|
449
|
+
req.destroy();
|
|
450
|
+
finish(undefined, new Error(`Request timeout after ${TIMEOUTS.REQUEST}ms`));
|
|
361
451
|
});
|
|
362
|
-
req.on('error',
|
|
452
|
+
req.on('error', (err) => finish(undefined, err));
|
|
453
|
+
timeoutId = setTimeout(() => {
|
|
454
|
+
req.destroy();
|
|
455
|
+
finish(undefined, new Error(`Request timeout after ${TIMEOUTS.REQUEST}ms`));
|
|
456
|
+
}, TIMEOUTS.REQUEST);
|
|
363
457
|
if (request.body && request.method !== 'GET') {
|
|
364
458
|
req.write(Buffer.from(request.body, 'base64'));
|
|
365
459
|
}
|
|
366
460
|
req.end();
|
|
367
461
|
});
|
|
368
462
|
}
|
|
369
|
-
sendMessage(message) {
|
|
370
|
-
if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
|
|
371
|
-
this.ws.send(JSON.stringify(message));
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
463
|
attemptReconnect() {
|
|
375
|
-
if (this.reconnectAttempts <
|
|
464
|
+
if (this.reconnectAttempts < RECONNECT.MAX_ATTEMPTS) {
|
|
376
465
|
this.reconnectAttempts++;
|
|
377
466
|
setTimeout(async () => {
|
|
378
467
|
try {
|
|
379
468
|
await this.connect();
|
|
380
469
|
}
|
|
381
|
-
catch (
|
|
382
|
-
},
|
|
470
|
+
catch (_a) { }
|
|
471
|
+
}, TIMEOUTS.RECONNECT_DELAY);
|
|
383
472
|
}
|
|
384
473
|
else {
|
|
385
474
|
this.emit("max_reconnect_attempts");
|
|
386
475
|
}
|
|
387
476
|
}
|
|
388
477
|
async stop() {
|
|
389
|
-
|
|
478
|
+
this.stopHeartbeat();
|
|
479
|
+
this.cleanupAllClients();
|
|
480
|
+
if (this.ws) {
|
|
390
481
|
try {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
try {
|
|
394
|
-
if (client.readyState === ws_1.default.OPEN) {
|
|
395
|
-
client.close();
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
catch (error) {
|
|
399
|
-
console.error(`Error closing WebSocket client ${id}:`, error);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
this.webSocketClients.clear();
|
|
403
|
-
// Close all SSE clients
|
|
404
|
-
for (const [id, client] of this.sseClients.entries()) {
|
|
405
|
-
try {
|
|
406
|
-
client.destroy();
|
|
407
|
-
}
|
|
408
|
-
catch (error) {
|
|
409
|
-
console.error(`Error destroying SSE client ${id}:`, error);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
this.sseClients.clear();
|
|
413
|
-
// Close main WebSocket connection
|
|
414
|
-
if (this.ws) {
|
|
415
|
-
try {
|
|
416
|
-
if (this.ws.readyState === ws_1.default.OPEN) {
|
|
417
|
-
this.ws.close();
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
catch (error) {
|
|
421
|
-
console.error('Error closing main WebSocket:', error);
|
|
422
|
-
}
|
|
423
|
-
this.ws = null;
|
|
424
|
-
}
|
|
425
|
-
// Destroy the HTTP agent to close pooled connections
|
|
426
|
-
if (this.httpAgent) {
|
|
427
|
-
this.httpAgent.destroy();
|
|
428
|
-
}
|
|
429
|
-
// Wait for all connections to close properly
|
|
430
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
431
|
-
resolve();
|
|
432
|
-
}
|
|
433
|
-
catch (error) {
|
|
434
|
-
console.error('Error in stop method:', error);
|
|
435
|
-
resolve(); // Resolve anyway to not block cleaning up
|
|
482
|
+
if (this.ws.readyState === ws_1.default.OPEN)
|
|
483
|
+
this.ws.close();
|
|
436
484
|
}
|
|
437
|
-
|
|
485
|
+
catch (_a) { }
|
|
486
|
+
this.ws = null;
|
|
487
|
+
}
|
|
488
|
+
this.tunnelUrl = null;
|
|
489
|
+
this.httpAgent.destroy();
|
|
490
|
+
await new Promise(resolve => setTimeout(resolve, TIMEOUTS.SHUTDOWN_GRACE));
|
|
438
491
|
}
|
|
439
492
|
}
|
|
440
493
|
exports.DainTunnel = DainTunnel;
|