@dainprotocol/tunnel 1.1.26 → 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 +2 -17
- package/dist/client/index.js +174 -266
- package/dist/server/index.d.ts +6 -4
- package/dist/server/index.js +255 -308
- package/package.json +1 -1
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,63 +28,53 @@ 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
|
-
// Heartbeat interval for client-side liveness
|
|
25
33
|
this.heartbeatInterval = null;
|
|
26
|
-
this.HEARTBEAT_INTERVAL = 25000; // 25 seconds (less than server's 30s)
|
|
27
|
-
// Client-side timeout (less than server's 30s to respond before server times out)
|
|
28
|
-
this.REQUEST_TIMEOUT = 25000;
|
|
29
|
-
// Parse API key to extract agentId and secret
|
|
30
34
|
const parsed = (0, auth_1.parseAPIKey)(apiKey);
|
|
31
35
|
if (!parsed) {
|
|
32
36
|
throw new Error('Invalid API key format. Expected: sk_agent_{agentId}_{orgId}_{secret}');
|
|
33
37
|
}
|
|
34
38
|
this.apiKey = apiKey;
|
|
35
|
-
this.tunnelId = `${parsed.orgId}_${parsed.agentId}`;
|
|
36
|
-
this.secret = parsed.secret;
|
|
37
|
-
// High-frequency optimization: Create reusable HTTP agent with connection pooling
|
|
38
|
-
// Aligned with server's MAX_CONCURRENT_REQUESTS_PER_TUNNEL = 100
|
|
39
|
+
this.tunnelId = `${parsed.orgId}_${parsed.agentId}`;
|
|
40
|
+
this.secret = parsed.secret;
|
|
39
41
|
this.httpAgent = new http_1.default.Agent({
|
|
40
42
|
keepAlive: true,
|
|
41
|
-
keepAliveMsecs: 30000,
|
|
42
|
-
maxSockets: 100,
|
|
43
|
-
maxFreeSockets: 20,
|
|
43
|
+
keepAliveMsecs: 30000,
|
|
44
|
+
maxSockets: 100,
|
|
45
|
+
maxFreeSockets: 20,
|
|
44
46
|
});
|
|
45
47
|
}
|
|
46
|
-
/**
|
|
47
|
-
* Sign a challenge using HMAC-SHA256
|
|
48
|
-
* @private
|
|
49
|
-
*/
|
|
50
48
|
signChallenge(challenge) {
|
|
51
|
-
return (0, crypto_1.createHmac)('sha256', this.secret)
|
|
52
|
-
|
|
53
|
-
|
|
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;
|
|
54
63
|
}
|
|
55
|
-
/**
|
|
56
|
-
* Start client-side heartbeat to detect connection issues early
|
|
57
|
-
* @private
|
|
58
|
-
*/
|
|
59
64
|
startHeartbeat() {
|
|
60
|
-
this.stopHeartbeat();
|
|
65
|
+
this.stopHeartbeat();
|
|
61
66
|
this.heartbeatInterval = setInterval(() => {
|
|
62
|
-
|
|
67
|
+
var _a;
|
|
68
|
+
if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
|
|
63
69
|
try {
|
|
64
70
|
this.ws.ping();
|
|
65
71
|
}
|
|
66
|
-
catch (
|
|
67
|
-
// Ping failed, connection might be dead
|
|
72
|
+
catch (_b) {
|
|
68
73
|
this.emit("error", new Error("Heartbeat ping failed"));
|
|
69
74
|
}
|
|
70
75
|
}
|
|
71
|
-
},
|
|
76
|
+
}, TIMEOUTS.HEARTBEAT);
|
|
72
77
|
}
|
|
73
|
-
/**
|
|
74
|
-
* Stop the client-side heartbeat
|
|
75
|
-
* @private
|
|
76
|
-
*/
|
|
77
78
|
stopHeartbeat() {
|
|
78
79
|
if (this.heartbeatInterval) {
|
|
79
80
|
clearInterval(this.heartbeatInterval);
|
|
@@ -88,25 +89,20 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
88
89
|
return new Promise((resolve, reject) => {
|
|
89
90
|
let resolved = false;
|
|
90
91
|
let connectionTimeoutId = null;
|
|
91
|
-
const
|
|
92
|
+
const finish = (value, error) => {
|
|
93
|
+
if (resolved)
|
|
94
|
+
return;
|
|
95
|
+
resolved = true;
|
|
92
96
|
if (connectionTimeoutId) {
|
|
93
97
|
clearTimeout(connectionTimeoutId);
|
|
94
98
|
connectionTimeoutId = null;
|
|
95
99
|
}
|
|
96
|
-
|
|
97
|
-
const safeResolve = (value) => {
|
|
98
|
-
if (!resolved) {
|
|
99
|
-
resolved = true;
|
|
100
|
-
cleanup();
|
|
101
|
-
resolve(value);
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
const safeReject = (error) => {
|
|
105
|
-
if (!resolved) {
|
|
106
|
-
resolved = true;
|
|
107
|
-
cleanup();
|
|
100
|
+
if (error) {
|
|
108
101
|
reject(error);
|
|
109
102
|
}
|
|
103
|
+
else {
|
|
104
|
+
resolve(value);
|
|
105
|
+
}
|
|
110
106
|
};
|
|
111
107
|
try {
|
|
112
108
|
this.ws = new ws_1.default(this.serverUrl);
|
|
@@ -115,78 +111,74 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
115
111
|
try {
|
|
116
112
|
const challenge = await this.requestChallenge();
|
|
117
113
|
const signature = this.signChallenge(challenge);
|
|
118
|
-
this.
|
|
114
|
+
this.safeSend({
|
|
119
115
|
type: "start",
|
|
120
116
|
port: this.port,
|
|
121
117
|
challenge,
|
|
122
118
|
signature,
|
|
123
119
|
tunnelId: this.tunnelId,
|
|
124
|
-
apiKey: this.apiKey
|
|
120
|
+
apiKey: this.apiKey
|
|
125
121
|
});
|
|
126
|
-
// Start heartbeat after successful authentication
|
|
127
122
|
this.startHeartbeat();
|
|
128
123
|
this.emit("connected");
|
|
129
124
|
}
|
|
130
125
|
catch (err) {
|
|
131
|
-
|
|
126
|
+
finish(undefined, err);
|
|
132
127
|
}
|
|
133
128
|
});
|
|
134
129
|
this.ws.on("message", (data) => {
|
|
135
130
|
try {
|
|
136
|
-
this.handleMessage(JSON.parse(data),
|
|
131
|
+
this.handleMessage(JSON.parse(data), (url) => finish(url));
|
|
137
132
|
}
|
|
138
133
|
catch (err) {
|
|
139
|
-
|
|
134
|
+
finish(undefined, err);
|
|
140
135
|
}
|
|
141
136
|
});
|
|
142
137
|
this.ws.on("close", () => {
|
|
143
|
-
// Stop heartbeat on disconnect
|
|
144
138
|
this.stopHeartbeat();
|
|
145
|
-
|
|
146
|
-
for (const [, client] of this.sseClients) {
|
|
147
|
-
try {
|
|
148
|
-
client.destroy();
|
|
149
|
-
}
|
|
150
|
-
catch (e) { /* ignore */ }
|
|
151
|
-
}
|
|
152
|
-
this.sseClients.clear();
|
|
153
|
-
// Clean up all WebSocket clients
|
|
154
|
-
for (const [, client] of this.webSocketClients) {
|
|
155
|
-
try {
|
|
156
|
-
if (client.readyState === ws_1.default.OPEN)
|
|
157
|
-
client.close(1001);
|
|
158
|
-
}
|
|
159
|
-
catch (e) { /* ignore */ }
|
|
160
|
-
}
|
|
161
|
-
this.webSocketClients.clear();
|
|
139
|
+
this.cleanupAllClients();
|
|
162
140
|
if (this.tunnelUrl) {
|
|
163
141
|
this.emit("disconnected");
|
|
164
142
|
this.attemptReconnect();
|
|
165
143
|
}
|
|
166
144
|
else {
|
|
167
|
-
|
|
145
|
+
finish(undefined, new Error("Connection closed before tunnel established"));
|
|
168
146
|
}
|
|
169
147
|
});
|
|
170
148
|
this.ws.on("error", (error) => this.emit("error", error));
|
|
171
|
-
// Connection timeout with proper cleanup
|
|
172
149
|
connectionTimeoutId = setTimeout(() => {
|
|
150
|
+
var _a;
|
|
173
151
|
if (!resolved && (!this.ws || this.ws.readyState !== ws_1.default.OPEN)) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
try {
|
|
178
|
-
this.ws.terminate();
|
|
179
|
-
}
|
|
180
|
-
catch (e) { /* ignore */ }
|
|
152
|
+
finish(undefined, new Error("Connection timeout"));
|
|
153
|
+
try {
|
|
154
|
+
(_a = this.ws) === null || _a === void 0 ? void 0 : _a.terminate();
|
|
181
155
|
}
|
|
156
|
+
catch (_b) { }
|
|
182
157
|
}
|
|
183
|
-
},
|
|
158
|
+
}, TIMEOUTS.CONNECTION);
|
|
184
159
|
}
|
|
185
160
|
catch (err) {
|
|
186
|
-
|
|
161
|
+
finish(undefined, err);
|
|
187
162
|
}
|
|
188
163
|
});
|
|
189
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
|
+
}
|
|
190
182
|
async requestChallenge() {
|
|
191
183
|
return new Promise((resolve, reject) => {
|
|
192
184
|
if (!this.ws) {
|
|
@@ -195,38 +187,37 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
195
187
|
}
|
|
196
188
|
let resolved = false;
|
|
197
189
|
let timeoutId = null;
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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);
|
|
202
196
|
if (timeoutId) {
|
|
203
197
|
clearTimeout(timeoutId);
|
|
204
198
|
timeoutId = null;
|
|
205
199
|
}
|
|
200
|
+
if (error) {
|
|
201
|
+
reject(error);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
resolve(challenge);
|
|
205
|
+
}
|
|
206
206
|
};
|
|
207
207
|
const challengeHandler = (message) => {
|
|
208
208
|
try {
|
|
209
209
|
const data = JSON.parse(message);
|
|
210
|
-
if (data.type === "challenge"
|
|
211
|
-
|
|
212
|
-
cleanup();
|
|
213
|
-
resolve(data.challenge);
|
|
210
|
+
if (data.type === "challenge") {
|
|
211
|
+
finish(data.challenge);
|
|
214
212
|
}
|
|
215
213
|
}
|
|
216
|
-
catch (
|
|
217
|
-
// Ignore parse errors for non-challenge messages
|
|
218
|
-
}
|
|
214
|
+
catch (_a) { }
|
|
219
215
|
};
|
|
220
216
|
this.ws.on("message", challengeHandler);
|
|
221
217
|
this.ws.send(JSON.stringify({ type: "challenge_request" }));
|
|
222
|
-
// Add a timeout for the challenge request
|
|
223
218
|
timeoutId = setTimeout(() => {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
cleanup();
|
|
227
|
-
reject(new Error("Challenge request timeout"));
|
|
228
|
-
}
|
|
229
|
-
}, 5000);
|
|
219
|
+
finish(undefined, new Error("Challenge request timeout"));
|
|
220
|
+
}, TIMEOUTS.CHALLENGE);
|
|
230
221
|
});
|
|
231
222
|
}
|
|
232
223
|
handleMessage(message, resolve) {
|
|
@@ -256,141 +247,119 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
256
247
|
async handleRequest(request) {
|
|
257
248
|
try {
|
|
258
249
|
const response = await this.forwardRequest(request);
|
|
259
|
-
this.
|
|
250
|
+
this.safeSend(response);
|
|
260
251
|
this.emit("request_handled", { request, response });
|
|
261
252
|
}
|
|
262
253
|
catch (error) {
|
|
263
|
-
// Send error response back to server instead of just emitting an event
|
|
264
|
-
// This prevents 30-second timeouts when the local service is unreachable
|
|
265
254
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
266
|
-
|
|
255
|
+
this.safeSend({
|
|
267
256
|
type: "response",
|
|
268
257
|
requestId: request.id,
|
|
269
|
-
status: 502,
|
|
258
|
+
status: 502,
|
|
270
259
|
headers: { "content-type": "application/json" },
|
|
271
260
|
body: Buffer.from(JSON.stringify({
|
|
272
261
|
error: "Bad Gateway",
|
|
273
262
|
message: `Failed to forward request to local service: ${errorMessage}`
|
|
274
263
|
})).toString("base64")
|
|
275
|
-
};
|
|
276
|
-
this.sendMessage(errorResponse);
|
|
264
|
+
});
|
|
277
265
|
this.emit("request_error", { request, error });
|
|
278
266
|
}
|
|
279
267
|
}
|
|
280
268
|
handleWebSocketConnection(message) {
|
|
281
|
-
|
|
269
|
+
const sendWsEvent = (event, data) => {
|
|
270
|
+
this.safeSend({ type: 'websocket', id: message.id, event, data });
|
|
271
|
+
};
|
|
282
272
|
try {
|
|
283
273
|
const client = new ws_1.default(`ws://localhost:${this.port}${message.path}`, {
|
|
284
274
|
headers: message.headers
|
|
285
275
|
});
|
|
286
276
|
this.webSocketClients.set(message.id, client);
|
|
287
277
|
client.on('message', (data) => {
|
|
288
|
-
|
|
289
|
-
if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
|
|
290
|
-
this.ws.send(JSON.stringify({ type: 'websocket', id: message.id, event: 'message', data: data.toString('base64') }));
|
|
291
|
-
}
|
|
278
|
+
sendWsEvent('message', data.toString('base64'));
|
|
292
279
|
});
|
|
293
280
|
client.on('close', () => {
|
|
294
|
-
|
|
295
|
-
if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
|
|
296
|
-
this.ws.send(JSON.stringify({ type: 'websocket', id: message.id, event: 'close' }));
|
|
297
|
-
}
|
|
281
|
+
sendWsEvent('close');
|
|
298
282
|
this.webSocketClients.delete(message.id);
|
|
299
283
|
});
|
|
300
284
|
client.on('error', (error) => {
|
|
301
|
-
|
|
302
|
-
if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
|
|
303
|
-
this.ws.send(JSON.stringify({ type: 'websocket', id: message.id, event: 'error', data: error.message }));
|
|
304
|
-
}
|
|
285
|
+
sendWsEvent('error', error.message);
|
|
305
286
|
this.webSocketClients.delete(message.id);
|
|
306
287
|
});
|
|
307
288
|
this.emit("websocket_connection", { id: message.id, path: message.path });
|
|
308
289
|
}
|
|
309
290
|
catch (error) {
|
|
310
|
-
|
|
311
|
-
this.ws.send(JSON.stringify({ type: 'websocket', id: message.id, event: 'error', data: error.message }));
|
|
312
|
-
}
|
|
291
|
+
sendWsEvent('error', error.message);
|
|
313
292
|
}
|
|
314
293
|
}
|
|
315
294
|
handleWebSocketMessage(message) {
|
|
316
295
|
const client = this.webSocketClients.get(message.id);
|
|
317
296
|
if (!client)
|
|
318
297
|
return;
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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;
|
|
330
310
|
}
|
|
331
311
|
}
|
|
332
312
|
handleSSEConnection(message) {
|
|
333
|
-
|
|
313
|
+
const sendSseEvent = (event, data = '') => {
|
|
314
|
+
this.safeSend({ type: 'sse', id: message.id, event, data });
|
|
315
|
+
};
|
|
334
316
|
try {
|
|
317
|
+
const headers = { ...message.headers };
|
|
318
|
+
delete headers.host;
|
|
319
|
+
headers['accept-encoding'] = 'identity';
|
|
335
320
|
const req = http_1.default.request({
|
|
336
321
|
hostname: 'localhost',
|
|
337
322
|
port: this.port,
|
|
338
323
|
path: message.path,
|
|
339
324
|
method: message.method || 'GET',
|
|
340
|
-
headers
|
|
325
|
+
headers,
|
|
341
326
|
agent: this.httpAgent,
|
|
342
327
|
}, (res) => {
|
|
343
|
-
var _a;
|
|
344
|
-
// Non-200 response - forward error to tunnel server
|
|
345
328
|
if (res.statusCode !== 200) {
|
|
346
329
|
let errorBody = '';
|
|
347
330
|
res.on('data', (chunk) => { errorBody += chunk.toString(); });
|
|
348
331
|
res.on('end', () => {
|
|
349
|
-
|
|
350
|
-
if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
|
|
351
|
-
this.ws.send(JSON.stringify({
|
|
352
|
-
type: 'sse', id: message.id, event: 'error',
|
|
353
|
-
data: `Status ${res.statusCode}: ${errorBody.substring(0, 200)}`
|
|
354
|
-
}));
|
|
355
|
-
}
|
|
332
|
+
sendSseEvent('error', `Status ${res.statusCode}: ${errorBody.substring(0, 200)}`);
|
|
356
333
|
});
|
|
357
334
|
return;
|
|
358
335
|
}
|
|
359
|
-
// Optimize TCP for streaming
|
|
360
336
|
const socket = res.socket || res.connection;
|
|
361
337
|
if (socket === null || socket === void 0 ? void 0 : socket.setNoDelay)
|
|
362
338
|
socket.setNoDelay(true);
|
|
363
|
-
|
|
364
|
-
if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
|
|
365
|
-
this.ws.send(JSON.stringify({ type: 'sse', id: message.id, event: 'connected', data: '' }));
|
|
366
|
-
}
|
|
367
|
-
// Stream SSE events to tunnel server
|
|
339
|
+
sendSseEvent('connected');
|
|
368
340
|
let buffer = '';
|
|
369
|
-
// Helper to process SSE events from buffer
|
|
370
341
|
const processBuffer = () => {
|
|
371
|
-
var _a;
|
|
372
|
-
// FIX: Normalize CRLF to LF for cross-platform SSE compatibility
|
|
373
|
-
// Some SSE implementations use \r\n\r\n instead of \n\n
|
|
374
342
|
buffer = buffer.replace(/\r\n/g, '\n');
|
|
375
343
|
while (buffer.includes('\n\n')) {
|
|
376
344
|
const idx = buffer.indexOf('\n\n');
|
|
377
345
|
const msgData = buffer.substring(0, idx);
|
|
378
346
|
buffer = buffer.substring(idx + 2);
|
|
379
|
-
// Skip empty messages and keepalive comments
|
|
380
347
|
if (!msgData.trim() || msgData.startsWith(':'))
|
|
381
348
|
continue;
|
|
382
|
-
let event = 'message'
|
|
349
|
+
let event = 'message';
|
|
350
|
+
let data = '';
|
|
383
351
|
for (const line of msgData.split('\n')) {
|
|
384
|
-
if (line.startsWith('event:'))
|
|
352
|
+
if (line.startsWith('event:')) {
|
|
385
353
|
event = line.substring(6).trim();
|
|
386
|
-
|
|
354
|
+
}
|
|
355
|
+
else if (line.startsWith('data:')) {
|
|
387
356
|
data += line.substring(5).trim() + '\n';
|
|
357
|
+
}
|
|
388
358
|
}
|
|
389
359
|
if (data.endsWith('\n'))
|
|
390
360
|
data = data.slice(0, -1);
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
this.ws.send(JSON.stringify({ type: 'sse', id: message.id, event, data }));
|
|
361
|
+
if (data) {
|
|
362
|
+
sendSseEvent(event, data);
|
|
394
363
|
}
|
|
395
364
|
}
|
|
396
365
|
};
|
|
@@ -399,34 +368,29 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
399
368
|
processBuffer();
|
|
400
369
|
});
|
|
401
370
|
res.on('end', () => {
|
|
402
|
-
var _a;
|
|
403
|
-
// Process any remaining data in buffer (might be missing final \n\n)
|
|
404
371
|
if (buffer.trim()) {
|
|
405
|
-
// Add missing newlines to ensure processing
|
|
406
372
|
buffer += '\n\n';
|
|
407
373
|
processBuffer();
|
|
408
374
|
}
|
|
409
|
-
|
|
410
|
-
this.ws.send(JSON.stringify({ type: 'sse', id: message.id, event: 'close', data: '' }));
|
|
411
|
-
}
|
|
375
|
+
sendSseEvent('close');
|
|
412
376
|
});
|
|
413
377
|
});
|
|
378
|
+
req.on('socket', (socket) => {
|
|
379
|
+
socket.setNoDelay(true);
|
|
380
|
+
socket.setTimeout(0);
|
|
381
|
+
});
|
|
414
382
|
req.on('error', (error) => {
|
|
415
|
-
|
|
416
|
-
if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
|
|
417
|
-
this.ws.send(JSON.stringify({ type: 'sse', id: message.id, event: 'error', data: error.message }));
|
|
418
|
-
}
|
|
383
|
+
sendSseEvent('error', error.message);
|
|
419
384
|
});
|
|
420
|
-
if (message.body && message.method !== 'GET')
|
|
385
|
+
if (message.body && message.method !== 'GET') {
|
|
421
386
|
req.write(Buffer.from(message.body, 'base64'));
|
|
387
|
+
}
|
|
422
388
|
req.end();
|
|
423
389
|
this.sseClients.set(message.id, req);
|
|
424
390
|
this.emit("sse_connection", { id: message.id, path: message.path });
|
|
425
391
|
}
|
|
426
392
|
catch (error) {
|
|
427
|
-
|
|
428
|
-
this.ws.send(JSON.stringify({ type: 'sse', id: message.id, event: 'error', data: error.message }));
|
|
429
|
-
}
|
|
393
|
+
sendSseEvent('error', error.message);
|
|
430
394
|
}
|
|
431
395
|
}
|
|
432
396
|
handleSSEClose(message) {
|
|
@@ -440,25 +404,20 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
440
404
|
return new Promise((resolve, reject) => {
|
|
441
405
|
let resolved = false;
|
|
442
406
|
let timeoutId = null;
|
|
443
|
-
const
|
|
407
|
+
const finish = (response, error) => {
|
|
408
|
+
if (resolved)
|
|
409
|
+
return;
|
|
410
|
+
resolved = true;
|
|
444
411
|
if (timeoutId) {
|
|
445
412
|
clearTimeout(timeoutId);
|
|
446
413
|
timeoutId = null;
|
|
447
414
|
}
|
|
448
|
-
|
|
449
|
-
const safeResolve = (response) => {
|
|
450
|
-
if (!resolved) {
|
|
451
|
-
resolved = true;
|
|
452
|
-
cleanup();
|
|
453
|
-
resolve(response);
|
|
454
|
-
}
|
|
455
|
-
};
|
|
456
|
-
const safeReject = (error) => {
|
|
457
|
-
if (!resolved) {
|
|
458
|
-
resolved = true;
|
|
459
|
-
cleanup();
|
|
415
|
+
if (error) {
|
|
460
416
|
reject(error);
|
|
461
417
|
}
|
|
418
|
+
else {
|
|
419
|
+
resolve(response);
|
|
420
|
+
}
|
|
462
421
|
};
|
|
463
422
|
const options = {
|
|
464
423
|
hostname: 'localhost',
|
|
@@ -466,120 +425,69 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
466
425
|
path: request.path,
|
|
467
426
|
method: request.method,
|
|
468
427
|
headers: request.headers,
|
|
469
|
-
agent: this.httpAgent,
|
|
470
|
-
timeout:
|
|
428
|
+
agent: this.httpAgent,
|
|
429
|
+
timeout: TIMEOUTS.REQUEST,
|
|
471
430
|
};
|
|
472
431
|
const req = http_1.default.request(options, (res) => {
|
|
473
|
-
|
|
474
|
-
res.on('data', (chunk) =>
|
|
475
|
-
body = Buffer.concat([body, chunk]);
|
|
476
|
-
});
|
|
432
|
+
const chunks = [];
|
|
433
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
477
434
|
res.on('end', () => {
|
|
478
435
|
const headers = { ...res.headers };
|
|
479
436
|
delete headers['transfer-encoding'];
|
|
480
437
|
delete headers['content-length'];
|
|
481
|
-
|
|
438
|
+
finish({
|
|
482
439
|
type: 'response',
|
|
483
440
|
requestId: request.id,
|
|
484
441
|
status: res.statusCode,
|
|
485
442
|
headers,
|
|
486
|
-
body:
|
|
443
|
+
body: Buffer.concat(chunks).toString('base64'),
|
|
487
444
|
});
|
|
488
445
|
});
|
|
489
|
-
res.on('error',
|
|
446
|
+
res.on('error', (err) => finish(undefined, err));
|
|
490
447
|
});
|
|
491
|
-
// Handle request timeout
|
|
492
448
|
req.on('timeout', () => {
|
|
493
449
|
req.destroy();
|
|
494
|
-
|
|
450
|
+
finish(undefined, new Error(`Request timeout after ${TIMEOUTS.REQUEST}ms`));
|
|
495
451
|
});
|
|
496
|
-
req.on('error',
|
|
497
|
-
// Additional safety timeout
|
|
452
|
+
req.on('error', (err) => finish(undefined, err));
|
|
498
453
|
timeoutId = setTimeout(() => {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
}
|
|
503
|
-
}, this.REQUEST_TIMEOUT);
|
|
454
|
+
req.destroy();
|
|
455
|
+
finish(undefined, new Error(`Request timeout after ${TIMEOUTS.REQUEST}ms`));
|
|
456
|
+
}, TIMEOUTS.REQUEST);
|
|
504
457
|
if (request.body && request.method !== 'GET') {
|
|
505
458
|
req.write(Buffer.from(request.body, 'base64'));
|
|
506
459
|
}
|
|
507
460
|
req.end();
|
|
508
461
|
});
|
|
509
462
|
}
|
|
510
|
-
sendMessage(message) {
|
|
511
|
-
if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
|
|
512
|
-
this.ws.send(JSON.stringify(message));
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
463
|
attemptReconnect() {
|
|
516
|
-
if (this.reconnectAttempts <
|
|
464
|
+
if (this.reconnectAttempts < RECONNECT.MAX_ATTEMPTS) {
|
|
517
465
|
this.reconnectAttempts++;
|
|
518
466
|
setTimeout(async () => {
|
|
519
467
|
try {
|
|
520
468
|
await this.connect();
|
|
521
469
|
}
|
|
522
|
-
catch (
|
|
523
|
-
},
|
|
470
|
+
catch (_a) { }
|
|
471
|
+
}, TIMEOUTS.RECONNECT_DELAY);
|
|
524
472
|
}
|
|
525
473
|
else {
|
|
526
474
|
this.emit("max_reconnect_attempts");
|
|
527
475
|
}
|
|
528
476
|
}
|
|
529
477
|
async stop() {
|
|
530
|
-
|
|
478
|
+
this.stopHeartbeat();
|
|
479
|
+
this.cleanupAllClients();
|
|
480
|
+
if (this.ws) {
|
|
531
481
|
try {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
// Close all WebSocket clients
|
|
535
|
-
for (const [id, client] of this.webSocketClients.entries()) {
|
|
536
|
-
try {
|
|
537
|
-
if (client.readyState === ws_1.default.OPEN) {
|
|
538
|
-
client.close();
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
catch (error) {
|
|
542
|
-
console.error(`Error closing WebSocket client ${id}:`, error);
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
this.webSocketClients.clear();
|
|
546
|
-
// Close all SSE clients
|
|
547
|
-
for (const [id, client] of this.sseClients.entries()) {
|
|
548
|
-
try {
|
|
549
|
-
client.destroy();
|
|
550
|
-
}
|
|
551
|
-
catch (error) {
|
|
552
|
-
console.error(`Error destroying SSE client ${id}:`, error);
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
this.sseClients.clear();
|
|
556
|
-
// Close main WebSocket connection
|
|
557
|
-
if (this.ws) {
|
|
558
|
-
try {
|
|
559
|
-
if (this.ws.readyState === ws_1.default.OPEN) {
|
|
560
|
-
this.ws.close();
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
catch (error) {
|
|
564
|
-
console.error('Error closing main WebSocket:', error);
|
|
565
|
-
}
|
|
566
|
-
this.ws = null;
|
|
567
|
-
}
|
|
568
|
-
// Reset tunnel URL so reconnect doesn't happen
|
|
569
|
-
this.tunnelUrl = null;
|
|
570
|
-
// Destroy the HTTP agent to close pooled connections
|
|
571
|
-
if (this.httpAgent) {
|
|
572
|
-
this.httpAgent.destroy();
|
|
573
|
-
}
|
|
574
|
-
// Wait for all connections to close properly
|
|
575
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
576
|
-
resolve();
|
|
577
|
-
}
|
|
578
|
-
catch (error) {
|
|
579
|
-
console.error('Error in stop method:', error);
|
|
580
|
-
resolve(); // Resolve anyway to not block cleaning up
|
|
482
|
+
if (this.ws.readyState === ws_1.default.OPEN)
|
|
483
|
+
this.ws.close();
|
|
581
484
|
}
|
|
582
|
-
|
|
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));
|
|
583
491
|
}
|
|
584
492
|
}
|
|
585
493
|
exports.DainTunnel = DainTunnel;
|