@dainprotocol/tunnel 1.1.14 → 1.1.17
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 +1 -0
- package/dist/client/index.js +77 -155
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +140 -168
- package/package.json +3 -3
package/dist/client/index.d.ts
CHANGED
package/dist/client/index.js
CHANGED
|
@@ -29,6 +29,13 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
29
29
|
this.apiKey = apiKey;
|
|
30
30
|
this.tunnelId = `${parsed.orgId}_${parsed.agentId}`; // orgId_agentId to prevent collisions
|
|
31
31
|
this.secret = parsed.secret; // secret for HMAC signatures
|
|
32
|
+
// High-frequency optimization: Create reusable HTTP agent with connection pooling
|
|
33
|
+
this.httpAgent = new http_1.default.Agent({
|
|
34
|
+
keepAlive: true,
|
|
35
|
+
keepAliveMsecs: 30000, // Keep connections alive for 30s
|
|
36
|
+
maxSockets: 50, // Allow up to 50 concurrent connections to local service
|
|
37
|
+
maxFreeSockets: 10, // Keep up to 10 idle connections
|
|
38
|
+
});
|
|
32
39
|
}
|
|
33
40
|
/**
|
|
34
41
|
* Sign a challenge using HMAC-SHA256
|
|
@@ -46,10 +53,8 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
46
53
|
async connect() {
|
|
47
54
|
return new Promise((resolve, reject) => {
|
|
48
55
|
try {
|
|
49
|
-
console.log(`Connecting to WebSocket server: ${this.serverUrl}`);
|
|
50
56
|
this.ws = new ws_1.default(this.serverUrl);
|
|
51
57
|
this.ws.on("open", async () => {
|
|
52
|
-
console.log('WebSocket connection opened');
|
|
53
58
|
this.reconnectAttempts = 0;
|
|
54
59
|
try {
|
|
55
60
|
const challenge = await this.requestChallenge();
|
|
@@ -65,46 +70,51 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
65
70
|
this.emit("connected");
|
|
66
71
|
}
|
|
67
72
|
catch (err) {
|
|
68
|
-
console.error('Error during challenge-response:', err);
|
|
69
73
|
reject(err);
|
|
70
74
|
}
|
|
71
75
|
});
|
|
72
76
|
this.ws.on("message", (data) => {
|
|
73
77
|
try {
|
|
74
|
-
|
|
75
|
-
this.handleMessage(message, resolve);
|
|
78
|
+
this.handleMessage(JSON.parse(data), resolve);
|
|
76
79
|
}
|
|
77
80
|
catch (err) {
|
|
78
|
-
console.error('Error handling message:', err);
|
|
79
81
|
reject(err);
|
|
80
82
|
}
|
|
81
83
|
});
|
|
82
84
|
this.ws.on("close", () => {
|
|
83
|
-
|
|
85
|
+
// Clean up all active SSE connections
|
|
86
|
+
for (const [, client] of this.sseClients) {
|
|
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();
|
|
84
102
|
if (this.tunnelUrl) {
|
|
85
103
|
this.emit("disconnected");
|
|
86
104
|
this.attemptReconnect();
|
|
87
105
|
}
|
|
88
106
|
else {
|
|
89
|
-
|
|
90
|
-
reject(new Error("Connection closed before tunnel was established"));
|
|
107
|
+
reject(new Error("Connection closed before tunnel established"));
|
|
91
108
|
}
|
|
92
109
|
});
|
|
93
|
-
this.ws.on("error", (error) =>
|
|
94
|
-
// Prevent unhandled error events
|
|
95
|
-
// Don't reject here, the close handler will be called after an error
|
|
96
|
-
this.emit("error", error);
|
|
97
|
-
});
|
|
98
|
-
// Add a timeout to reject the promise if connection takes too long
|
|
110
|
+
this.ws.on("error", (error) => this.emit("error", error));
|
|
99
111
|
setTimeout(() => {
|
|
100
112
|
if (!this.ws || this.ws.readyState !== ws_1.default.OPEN) {
|
|
101
|
-
|
|
102
|
-
reject(timeoutError);
|
|
113
|
+
reject(new Error("Connection timeout"));
|
|
103
114
|
}
|
|
104
115
|
}, 10000);
|
|
105
116
|
}
|
|
106
117
|
catch (err) {
|
|
107
|
-
console.error('Error creating WebSocket:', err);
|
|
108
118
|
reject(err);
|
|
109
119
|
}
|
|
110
120
|
});
|
|
@@ -170,65 +180,37 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
170
180
|
}
|
|
171
181
|
}
|
|
172
182
|
handleWebSocketConnection(message) {
|
|
183
|
+
var _a;
|
|
173
184
|
try {
|
|
174
|
-
console.log(`Creating local WebSocket connection to: ws://localhost:${this.port}${message.path}`);
|
|
175
|
-
// Create a WebSocket connection to the local server
|
|
176
185
|
const client = new ws_1.default(`ws://localhost:${this.port}${message.path}`, {
|
|
177
186
|
headers: message.headers
|
|
178
187
|
});
|
|
179
|
-
// Store the client
|
|
180
188
|
this.webSocketClients.set(message.id, client);
|
|
181
|
-
// Handle connection open
|
|
182
|
-
client.on('open', () => {
|
|
183
|
-
console.log(`Local WebSocket connection opened: ${message.id}`);
|
|
184
|
-
});
|
|
185
|
-
// Handle messages from the local server
|
|
186
189
|
client.on('message', (data) => {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
id: message.id,
|
|
191
|
-
event: 'message',
|
|
192
|
-
data: data.toString('base64')
|
|
193
|
-
}));
|
|
190
|
+
var _a;
|
|
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') }));
|
|
194
193
|
}
|
|
195
194
|
});
|
|
196
|
-
// Handle close
|
|
197
195
|
client.on('close', () => {
|
|
198
|
-
|
|
199
|
-
if (this.ws
|
|
200
|
-
this.ws.send(JSON.stringify({
|
|
201
|
-
type: 'websocket',
|
|
202
|
-
id: message.id,
|
|
203
|
-
event: 'close'
|
|
204
|
-
}));
|
|
196
|
+
var _a;
|
|
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' }));
|
|
205
199
|
}
|
|
206
200
|
this.webSocketClients.delete(message.id);
|
|
207
201
|
});
|
|
208
|
-
// Handle errors
|
|
209
202
|
client.on('error', (error) => {
|
|
210
|
-
|
|
211
|
-
if (this.ws
|
|
212
|
-
this.ws.send(JSON.stringify({
|
|
213
|
-
type: 'websocket',
|
|
214
|
-
id: message.id,
|
|
215
|
-
event: 'error',
|
|
216
|
-
data: error.message
|
|
217
|
-
}));
|
|
203
|
+
var _a;
|
|
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 }));
|
|
218
206
|
}
|
|
219
207
|
this.webSocketClients.delete(message.id);
|
|
220
208
|
});
|
|
221
209
|
this.emit("websocket_connection", { id: message.id, path: message.path });
|
|
222
210
|
}
|
|
223
211
|
catch (error) {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
this.ws.send(JSON.stringify({
|
|
227
|
-
type: 'websocket',
|
|
228
|
-
id: message.id,
|
|
229
|
-
event: 'error',
|
|
230
|
-
data: error.message
|
|
231
|
-
}));
|
|
212
|
+
if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
|
|
213
|
+
this.ws.send(JSON.stringify({ type: 'websocket', id: message.id, event: 'error', data: error.message }));
|
|
232
214
|
}
|
|
233
215
|
}
|
|
234
216
|
}
|
|
@@ -250,134 +232,80 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
250
232
|
}
|
|
251
233
|
}
|
|
252
234
|
handleSSEConnection(message) {
|
|
235
|
+
var _a;
|
|
253
236
|
try {
|
|
254
|
-
console.log(`[SSE E2E] Client: Establishing SSE connection ${message.id} to localhost:${this.port}${message.path}`);
|
|
255
|
-
// Create an EventSource-like stream to the local server
|
|
256
|
-
// Since Node.js doesn't have a built-in EventSource, we'll use HTTP
|
|
257
237
|
const options = {
|
|
258
238
|
hostname: 'localhost',
|
|
259
239
|
port: this.port,
|
|
260
240
|
path: message.path,
|
|
261
|
-
method: message.method || 'GET',
|
|
241
|
+
method: message.method || 'GET',
|
|
262
242
|
headers: message.headers,
|
|
243
|
+
agent: this.httpAgent,
|
|
263
244
|
};
|
|
264
245
|
const req = http_1.default.request(options, (res) => {
|
|
246
|
+
var _a;
|
|
265
247
|
if (res.statusCode !== 200) {
|
|
266
|
-
|
|
267
|
-
if (this.ws
|
|
268
|
-
this.ws.send(JSON.stringify({
|
|
269
|
-
type: 'sse',
|
|
270
|
-
id: message.id,
|
|
271
|
-
event: 'error',
|
|
272
|
-
data: `Server responded with status code ${res.statusCode}`
|
|
273
|
-
}));
|
|
248
|
+
res.resume();
|
|
249
|
+
if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
|
|
250
|
+
this.ws.send(JSON.stringify({ type: 'sse', id: message.id, event: 'error', data: `Status ${res.statusCode}` }));
|
|
274
251
|
}
|
|
275
252
|
return;
|
|
276
253
|
}
|
|
277
|
-
// Optimize socket for low-latency streaming
|
|
278
254
|
const socket = res.socket || res.connection;
|
|
279
|
-
if (socket
|
|
280
|
-
socket.setNoDelay(true);
|
|
281
|
-
}
|
|
282
|
-
console.log(`[SSE E2E] Client: Service accepted SSE connection ${message.id}, streaming started`);
|
|
283
|
-
// Process SSE stream
|
|
255
|
+
if (socket === null || socket === void 0 ? void 0 : socket.setNoDelay)
|
|
256
|
+
socket.setNoDelay(true);
|
|
284
257
|
let buffer = '';
|
|
285
|
-
let eventCount = 0;
|
|
286
258
|
res.on('data', (chunk) => {
|
|
287
|
-
|
|
288
|
-
buffer +=
|
|
289
|
-
// Process complete SSE messages
|
|
259
|
+
var _a;
|
|
260
|
+
buffer += chunk.toString();
|
|
290
261
|
while (buffer.includes('\n\n')) {
|
|
291
|
-
const
|
|
292
|
-
const
|
|
293
|
-
buffer = buffer.substring(
|
|
294
|
-
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
let data = '';
|
|
298
|
-
for (const line of lines) {
|
|
299
|
-
if (line.startsWith('event:')) {
|
|
262
|
+
const idx = buffer.indexOf('\n\n');
|
|
263
|
+
const msgData = buffer.substring(0, idx);
|
|
264
|
+
buffer = buffer.substring(idx + 2);
|
|
265
|
+
let event = 'message', data = '';
|
|
266
|
+
for (const line of msgData.split('\n')) {
|
|
267
|
+
if (line.startsWith('event:'))
|
|
300
268
|
event = line.substring(6).trim();
|
|
301
|
-
|
|
302
|
-
else if (line.startsWith('data:')) {
|
|
269
|
+
else if (line.startsWith('data:'))
|
|
303
270
|
data += line.substring(5).trim() + '\n';
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
// Remove trailing newline
|
|
307
|
-
if (data.endsWith('\n')) {
|
|
308
|
-
data = data.substring(0, data.length - 1);
|
|
309
271
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
if (
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
// Forward to server
|
|
316
|
-
if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
|
|
317
|
-
this.ws.send(JSON.stringify({
|
|
318
|
-
type: 'sse',
|
|
319
|
-
id: message.id,
|
|
320
|
-
event,
|
|
321
|
-
data
|
|
322
|
-
}));
|
|
272
|
+
if (data.endsWith('\n'))
|
|
273
|
+
data = data.slice(0, -1);
|
|
274
|
+
if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
|
|
275
|
+
this.ws.send(JSON.stringify({ type: 'sse', id: message.id, event, data }));
|
|
323
276
|
}
|
|
324
277
|
}
|
|
325
278
|
});
|
|
326
279
|
res.on('end', () => {
|
|
327
|
-
|
|
328
|
-
if (this.ws
|
|
329
|
-
this.ws.send(JSON.stringify({
|
|
330
|
-
type: 'sse',
|
|
331
|
-
id: message.id,
|
|
332
|
-
event: 'close',
|
|
333
|
-
data: 'Connection closed'
|
|
334
|
-
}));
|
|
280
|
+
var _a;
|
|
281
|
+
if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
|
|
282
|
+
this.ws.send(JSON.stringify({ type: 'sse', id: message.id, event: 'close', data: 'Connection closed' }));
|
|
335
283
|
}
|
|
336
284
|
});
|
|
337
285
|
});
|
|
338
286
|
req.on('error', (error) => {
|
|
339
|
-
|
|
340
|
-
if (this.ws
|
|
341
|
-
this.ws.send(JSON.stringify({
|
|
342
|
-
type: 'sse',
|
|
343
|
-
id: message.id,
|
|
344
|
-
event: 'error',
|
|
345
|
-
data: error.message
|
|
346
|
-
}));
|
|
287
|
+
var _a;
|
|
288
|
+
if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
|
|
289
|
+
this.ws.send(JSON.stringify({ type: 'sse', id: message.id, event: 'error', data: error.message }));
|
|
347
290
|
}
|
|
348
291
|
});
|
|
349
|
-
|
|
350
|
-
if (message.body && message.method !== 'GET') {
|
|
292
|
+
if (message.body && message.method !== 'GET')
|
|
351
293
|
req.write(Buffer.from(message.body, 'base64'));
|
|
352
|
-
}
|
|
353
294
|
req.end();
|
|
354
|
-
// Store a reference to abort the connection later if needed
|
|
355
295
|
this.sseClients.set(message.id, req);
|
|
356
296
|
this.emit("sse_connection", { id: message.id, path: message.path });
|
|
357
297
|
}
|
|
358
298
|
catch (error) {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
this.ws.send(JSON.stringify({
|
|
362
|
-
type: 'sse',
|
|
363
|
-
id: message.id,
|
|
364
|
-
event: 'error',
|
|
365
|
-
data: error.message
|
|
366
|
-
}));
|
|
299
|
+
if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
|
|
300
|
+
this.ws.send(JSON.stringify({ type: 'sse', id: message.id, event: 'error', data: error.message }));
|
|
367
301
|
}
|
|
368
302
|
}
|
|
369
303
|
}
|
|
370
304
|
handleSSEClose(message) {
|
|
371
|
-
console.log(`[SSE E2E] Client: Received close command from server for ${message.id}`);
|
|
372
305
|
const client = this.sseClients.get(message.id);
|
|
373
306
|
if (client) {
|
|
374
|
-
// Abort the request if it's still active
|
|
375
307
|
client.destroy();
|
|
376
308
|
this.sseClients.delete(message.id);
|
|
377
|
-
console.log(`[SSE E2E] Client: Destroyed local service connection for ${message.id}`);
|
|
378
|
-
}
|
|
379
|
-
else {
|
|
380
|
-
console.log(`[SSE E2E] Client: No active connection found for ${message.id} (already cleaned up)`);
|
|
381
309
|
}
|
|
382
310
|
}
|
|
383
311
|
forwardRequest(request) {
|
|
@@ -388,6 +316,7 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
388
316
|
path: request.path,
|
|
389
317
|
method: request.method,
|
|
390
318
|
headers: request.headers,
|
|
319
|
+
agent: this.httpAgent, // Use connection pooling
|
|
391
320
|
};
|
|
392
321
|
const req = http_1.default.request(options, (res) => {
|
|
393
322
|
let body = Buffer.from([]);
|
|
@@ -422,25 +351,14 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
422
351
|
attemptReconnect() {
|
|
423
352
|
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
424
353
|
this.reconnectAttempts++;
|
|
425
|
-
console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
|
|
426
354
|
setTimeout(async () => {
|
|
427
355
|
try {
|
|
428
356
|
await this.connect();
|
|
429
|
-
// If the connect succeeds but we don't have a tunnelUrl yet, we're still initializing
|
|
430
|
-
if (!this.tunnelUrl) {
|
|
431
|
-
console.log('Reconnected but tunnelUrl not set. Waiting for tunnel URL...');
|
|
432
|
-
}
|
|
433
|
-
else {
|
|
434
|
-
console.log(`Reconnected successfully. Tunnel URL: ${this.tunnelUrl}`);
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
catch (error) {
|
|
438
|
-
console.error(`Reconnection attempt ${this.reconnectAttempts} failed:`, error);
|
|
439
357
|
}
|
|
358
|
+
catch (error) { /* ignore */ }
|
|
440
359
|
}, this.reconnectDelay);
|
|
441
360
|
}
|
|
442
361
|
else {
|
|
443
|
-
console.log('Maximum reconnection attempts reached');
|
|
444
362
|
this.emit("max_reconnect_attempts");
|
|
445
363
|
}
|
|
446
364
|
}
|
|
@@ -481,6 +399,10 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
481
399
|
}
|
|
482
400
|
this.ws = null;
|
|
483
401
|
}
|
|
402
|
+
// Destroy the HTTP agent to close pooled connections
|
|
403
|
+
if (this.httpAgent) {
|
|
404
|
+
this.httpAgent.destroy();
|
|
405
|
+
}
|
|
484
406
|
// Wait for all connections to close properly
|
|
485
407
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
486
408
|
resolve();
|
package/dist/server/index.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ declare class DainTunnelServer {
|
|
|
9
9
|
private challenges;
|
|
10
10
|
private sseConnections;
|
|
11
11
|
private wsConnections;
|
|
12
|
+
private tunnelRequestCount;
|
|
13
|
+
private readonly MAX_CONCURRENT_REQUESTS_PER_TUNNEL;
|
|
12
14
|
constructor(hostname: string, port: number);
|
|
13
15
|
private setupExpressRoutes;
|
|
14
16
|
private setupWebSocketServer;
|
package/dist/server/index.js
CHANGED
|
@@ -6,12 +6,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
const express_1 = __importDefault(require("express"));
|
|
7
7
|
const http_1 = __importDefault(require("http"));
|
|
8
8
|
const ws_1 = __importDefault(require("ws"));
|
|
9
|
-
const uuid_1 = require("uuid");
|
|
10
9
|
const body_parser_1 = __importDefault(require("body-parser"));
|
|
11
|
-
const cors_1 = __importDefault(require("cors"));
|
|
12
10
|
const auth_1 = require("@dainprotocol/service-sdk/service/auth");
|
|
13
11
|
const crypto_1 = require("crypto");
|
|
14
12
|
const url_1 = require("url");
|
|
13
|
+
// High-frequency optimization: Fast ID generation using crypto.randomBytes
|
|
14
|
+
// ~10x faster than uuid v4 for high-frequency scenarios
|
|
15
|
+
let idCounter = 0;
|
|
16
|
+
function fastId() {
|
|
17
|
+
return `${Date.now().toString(36)}-${(idCounter++).toString(36)}-${(0, crypto_1.randomBytes)(4).toString('hex')}`;
|
|
18
|
+
}
|
|
15
19
|
/**
|
|
16
20
|
* Force immediate flush of SSE data to prevent buffering
|
|
17
21
|
* SSE requires immediate delivery; Node.js res.write() doesn't guarantee this
|
|
@@ -48,30 +52,40 @@ class DainTunnelServer {
|
|
|
48
52
|
this.challenges = new Map();
|
|
49
53
|
this.sseConnections = new Map();
|
|
50
54
|
this.wsConnections = new Map();
|
|
55
|
+
// High-frequency optimization: Track active request count per tunnel for backpressure
|
|
56
|
+
this.tunnelRequestCount = new Map();
|
|
57
|
+
this.MAX_CONCURRENT_REQUESTS_PER_TUNNEL = 100;
|
|
51
58
|
this.app = (0, express_1.default)();
|
|
52
59
|
this.server = http_1.default.createServer(this.app);
|
|
60
|
+
// High-frequency optimization: Configure server for better throughput
|
|
61
|
+
this.server.keepAliveTimeout = 65000; // Keep connections alive longer (default 5s)
|
|
62
|
+
this.server.headersTimeout = 66000; // Must be higher than keepAliveTimeout
|
|
63
|
+
this.server.maxHeadersCount = 100; // Limit header count for security
|
|
53
64
|
this.wss = new ws_1.default.Server({
|
|
54
65
|
server: this.server,
|
|
55
|
-
path: undefined // Allow connections on any path
|
|
66
|
+
path: undefined, // Allow connections on any path
|
|
67
|
+
// High-frequency optimization: Increase backlog and disable per-message deflate
|
|
68
|
+
backlog: 100,
|
|
69
|
+
perMessageDeflate: false, // Disable compression for lower latency
|
|
70
|
+
maxPayload: 100 * 1024 * 1024, // 100MB max payload
|
|
56
71
|
});
|
|
57
|
-
//
|
|
58
|
-
this.app.use((0, cors_1.default)({
|
|
59
|
-
origin: '*',
|
|
60
|
-
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
|
61
|
-
credentials: true
|
|
62
|
-
}));
|
|
63
|
-
// Update CORS middleware
|
|
72
|
+
// Single optimized CORS middleware (avoid duplicate cors() + manual headers)
|
|
64
73
|
this.app.use((req, res, next) => {
|
|
65
74
|
res.header("Access-Control-Allow-Origin", "*");
|
|
66
75
|
res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
|
|
67
|
-
res.header("Access-Control-Allow-Headers",
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
res.header("Access-Control-Allow-Headers", "X-DAIN-SIGNATURE, X-DAIN-SMART-ACCOUNT-PDA, X-DAIN-AGENT-ID, X-DAIN-ORG-ID, X-DAIN-ADDRESS, X-DAIN-TIMESTAMP, X-DAIN-WEBHOOK-URL, Content-Type, Authorization, Accept, Origin, X-Requested-With");
|
|
77
|
+
res.header("Access-Control-Allow-Credentials", "true");
|
|
78
|
+
if (req.method === "OPTIONS")
|
|
79
|
+
return res.sendStatus(204);
|
|
71
80
|
next();
|
|
72
81
|
});
|
|
73
|
-
// Add body-parser middleware
|
|
74
|
-
this.app.use(
|
|
82
|
+
// Add body-parser middleware (skip for GET/HEAD/OPTIONS which don't have bodies)
|
|
83
|
+
this.app.use((req, res, next) => {
|
|
84
|
+
if (req.method === 'GET' || req.method === 'HEAD' || req.method === 'OPTIONS') {
|
|
85
|
+
return next();
|
|
86
|
+
}
|
|
87
|
+
body_parser_1.default.raw({ type: "*/*", limit: "100mb" })(req, res, next);
|
|
88
|
+
});
|
|
75
89
|
this.setupExpressRoutes();
|
|
76
90
|
this.setupWebSocketServer();
|
|
77
91
|
}
|
|
@@ -83,25 +97,19 @@ class DainTunnelServer {
|
|
|
83
97
|
// Handle WebSocket connections from tunnel clients
|
|
84
98
|
this.wss.on("connection", (ws, req) => {
|
|
85
99
|
var _a;
|
|
86
|
-
console.log("New WebSocket connection");
|
|
87
100
|
try {
|
|
88
|
-
// Check if this is a tunnel client connection or a user connection to be proxied
|
|
89
101
|
const url = (0, url_1.parse)(req.url || '', true);
|
|
90
102
|
const pathParts = ((_a = url.pathname) === null || _a === void 0 ? void 0 : _a.split('/')) || [];
|
|
91
|
-
console.log(`WebSocket connection path: ${url.pathname}`);
|
|
92
103
|
// Client tunnel connection has no tunnelId in the path (or is at root)
|
|
93
104
|
if (!url.pathname || url.pathname === '/' || pathParts.length <= 1 || !pathParts[1]) {
|
|
94
|
-
console.log("Handling as tunnel client connection");
|
|
95
105
|
this.handleTunnelClientConnection(ws, req);
|
|
96
106
|
}
|
|
97
107
|
else {
|
|
98
|
-
// This is a WebSocket connection to be proxied through the tunnel
|
|
99
|
-
console.log(`Handling as proxied WebSocket connection for path: ${url.pathname}`);
|
|
100
108
|
this.handleProxiedWebSocketConnection(ws, req);
|
|
101
109
|
}
|
|
102
110
|
}
|
|
103
111
|
catch (error) {
|
|
104
|
-
console.error("
|
|
112
|
+
console.error("[Tunnel] WebSocket setup error:", error);
|
|
105
113
|
ws.close(1011, "Internal server error");
|
|
106
114
|
}
|
|
107
115
|
});
|
|
@@ -111,7 +119,6 @@ class DainTunnelServer {
|
|
|
111
119
|
ws.on("message", (message) => {
|
|
112
120
|
try {
|
|
113
121
|
const data = JSON.parse(message);
|
|
114
|
-
console.log(`Received WebSocket message: ${data.type}`);
|
|
115
122
|
if (data.type === "challenge_request") {
|
|
116
123
|
this.handleChallengeRequest(ws);
|
|
117
124
|
}
|
|
@@ -129,17 +136,12 @@ class DainTunnelServer {
|
|
|
129
136
|
}
|
|
130
137
|
}
|
|
131
138
|
catch (error) {
|
|
132
|
-
console.error("
|
|
139
|
+
console.error("[Tunnel] Message error:", error);
|
|
133
140
|
ws.close(1008, "Invalid message");
|
|
134
141
|
}
|
|
135
142
|
});
|
|
136
|
-
ws.on("close", () =>
|
|
137
|
-
|
|
138
|
-
this.removeTunnel(ws);
|
|
139
|
-
});
|
|
140
|
-
ws.on("error", (error) => {
|
|
141
|
-
console.error("WebSocket error:", error);
|
|
142
|
-
});
|
|
143
|
+
ws.on("close", () => this.removeTunnel(ws));
|
|
144
|
+
ws.on("error", (error) => console.error("[Tunnel] WS error:", error));
|
|
143
145
|
}
|
|
144
146
|
// Handle incoming WebSocket connections to be proxied through the tunnel
|
|
145
147
|
handleProxiedWebSocketConnection(ws, req) {
|
|
@@ -153,15 +155,12 @@ class DainTunnelServer {
|
|
|
153
155
|
const tunnelId = pathParts[1];
|
|
154
156
|
const remainingPath = '/' + pathParts.slice(2).join('/');
|
|
155
157
|
const tunnel = this.tunnels.get(tunnelId);
|
|
156
|
-
console.log(`Handling WebSocket connection for tunnel: ${tunnelId}, path: ${remainingPath}`);
|
|
157
|
-
console.log(`Available tunnels: ${Array.from(this.tunnels.keys()).join(', ')}`);
|
|
158
158
|
if (!tunnel) {
|
|
159
|
-
console.log(`Tunnel not found for WebSocket connection: ${tunnelId}`);
|
|
160
159
|
ws.close(1008, "Tunnel not found");
|
|
161
160
|
return;
|
|
162
161
|
}
|
|
163
162
|
// Create a WebSocket connection ID
|
|
164
|
-
const wsConnectionId = (
|
|
163
|
+
const wsConnectionId = fastId();
|
|
165
164
|
// Store this connection
|
|
166
165
|
this.wsConnections.set(wsConnectionId, {
|
|
167
166
|
clientSocket: ws,
|
|
@@ -205,10 +204,12 @@ class DainTunnelServer {
|
|
|
205
204
|
});
|
|
206
205
|
}
|
|
207
206
|
handleChallengeRequest(ws) {
|
|
208
|
-
const challenge = (
|
|
209
|
-
const challengeObj = { ws, challenge };
|
|
207
|
+
const challenge = fastId();
|
|
208
|
+
const challengeObj = { ws, challenge, timestamp: Date.now() };
|
|
210
209
|
this.challenges.set(challenge, challengeObj);
|
|
211
210
|
ws.send(JSON.stringify({ type: "challenge", challenge }));
|
|
211
|
+
// Auto-cleanup expired challenges (30 second TTL)
|
|
212
|
+
setTimeout(() => this.challenges.delete(challenge), 30000);
|
|
212
213
|
}
|
|
213
214
|
handleStartMessage(ws, data) {
|
|
214
215
|
const { challenge, signature, tunnelId, apiKey } = data;
|
|
@@ -250,34 +251,27 @@ class DainTunnelServer {
|
|
|
250
251
|
}
|
|
251
252
|
// If tunnel already exists, remove old one
|
|
252
253
|
if (this.tunnels.has(tunnelId)) {
|
|
253
|
-
console.log(`Tunnel ${tunnelId} already exists, replacing it`);
|
|
254
254
|
const oldTunnel = this.tunnels.get(tunnelId);
|
|
255
255
|
if (oldTunnel && oldTunnel.ws !== ws) {
|
|
256
256
|
oldTunnel.ws.close(1000, "Replaced by new connection");
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
259
|
this.tunnels.set(tunnelId, { id: tunnelId, ws });
|
|
260
|
-
console.log(`Tunnel added: ${tunnelId}`);
|
|
261
|
-
console.log(`Current tunnels: ${Array.from(this.tunnels.keys()).join(', ')}`);
|
|
262
260
|
// Save tunnel ID on the WebSocket object for easy lookup on close
|
|
263
261
|
ws.tunnelId = tunnelId;
|
|
264
|
-
// Add a periodic
|
|
262
|
+
// Add a periodic ping to keep the connection alive (silent - no logging)
|
|
265
263
|
const intervalId = setInterval(() => {
|
|
266
264
|
if (this.tunnels.has(tunnelId)) {
|
|
267
265
|
const tunnel = this.tunnels.get(tunnelId);
|
|
268
266
|
if (tunnel && tunnel.ws === ws && ws.readyState === ws_1.default.OPEN) {
|
|
269
|
-
console.log(`Tunnel ${tunnelId} still active`);
|
|
270
|
-
// Send a ping to keep the connection alive
|
|
271
267
|
try {
|
|
272
268
|
ws.ping();
|
|
273
269
|
}
|
|
274
270
|
catch (error) {
|
|
275
|
-
console.error(`Error sending ping to tunnel ${tunnelId}:`, error);
|
|
276
271
|
clearInterval(intervalId);
|
|
277
272
|
}
|
|
278
273
|
}
|
|
279
274
|
else {
|
|
280
|
-
console.log(`Tunnel ${tunnelId} exists but WebSocket state is invalid, cleaning up`);
|
|
281
275
|
clearInterval(intervalId);
|
|
282
276
|
if (tunnel && tunnel.ws === ws) {
|
|
283
277
|
this.tunnels.delete(tunnelId);
|
|
@@ -285,23 +279,19 @@ class DainTunnelServer {
|
|
|
285
279
|
}
|
|
286
280
|
}
|
|
287
281
|
else {
|
|
288
|
-
console.log(`Tunnel ${tunnelId} not found in periodic check`);
|
|
289
282
|
clearInterval(intervalId);
|
|
290
283
|
}
|
|
291
|
-
},
|
|
284
|
+
}, 30000); // Ping every 30 seconds (reduced frequency)
|
|
292
285
|
// Store the interval ID on the WebSocket object so we can clean it up
|
|
293
286
|
ws.keepAliveInterval = intervalId;
|
|
294
|
-
ws.on("close", () =>
|
|
295
|
-
console.log(`WebSocket for tunnel ${tunnelId} closed, clearing interval`);
|
|
296
|
-
clearInterval(intervalId);
|
|
297
|
-
});
|
|
287
|
+
ws.on("close", () => clearInterval(intervalId));
|
|
298
288
|
let tunnelUrl = `${this.hostname}`;
|
|
299
289
|
if (process.env.SKIP_PORT !== "true") {
|
|
300
290
|
tunnelUrl += `:${this.port}`;
|
|
301
291
|
}
|
|
302
292
|
tunnelUrl += `/${tunnelId}`;
|
|
303
293
|
ws.send(JSON.stringify({ type: "tunnelUrl", url: tunnelUrl }));
|
|
304
|
-
console.log(`
|
|
294
|
+
console.log(`[Tunnel] Created: ${tunnelUrl}`);
|
|
305
295
|
}
|
|
306
296
|
catch (error) {
|
|
307
297
|
console.error(`Error in handleStartMessage for tunnel ${tunnelId}:`, error);
|
|
@@ -311,8 +301,15 @@ class DainTunnelServer {
|
|
|
311
301
|
handleResponseMessage(data) {
|
|
312
302
|
const pendingRequest = this.pendingRequests.get(data.requestId);
|
|
313
303
|
if (pendingRequest) {
|
|
314
|
-
const { res, startTime } = pendingRequest;
|
|
304
|
+
const { res, startTime, tunnelId, timeoutId } = pendingRequest;
|
|
315
305
|
const endTime = Date.now();
|
|
306
|
+
// Clear the timeout since we received a response
|
|
307
|
+
if (timeoutId) {
|
|
308
|
+
clearTimeout(timeoutId);
|
|
309
|
+
}
|
|
310
|
+
// Decrement request counter for backpressure tracking
|
|
311
|
+
const currentCount = this.tunnelRequestCount.get(tunnelId) || 1;
|
|
312
|
+
this.tunnelRequestCount.set(tunnelId, Math.max(0, currentCount - 1));
|
|
316
313
|
const headers = { ...data.headers };
|
|
317
314
|
delete headers["transfer-encoding"];
|
|
318
315
|
delete headers["content-length"];
|
|
@@ -322,99 +319,57 @@ class DainTunnelServer {
|
|
|
322
319
|
.set(headers)
|
|
323
320
|
.set("Content-Length", bodyBuffer.length.toString())
|
|
324
321
|
.send(bodyBuffer);
|
|
325
|
-
console.log(`Request handled: ${data.requestId}, Duration: ${endTime - startTime}ms`);
|
|
326
322
|
this.pendingRequests.delete(data.requestId);
|
|
327
323
|
}
|
|
328
324
|
}
|
|
329
325
|
handleSSEMessage(data) {
|
|
330
326
|
const connection = this.sseConnections.get(data.id);
|
|
331
327
|
if (!connection) {
|
|
332
|
-
console.log(`[SSE
|
|
328
|
+
console.log(`[SSE Debug] handleSSEMessage: No connection found for ${data.id}, event=${data.event}`);
|
|
333
329
|
return;
|
|
334
330
|
}
|
|
335
331
|
const { res, tunnelId } = connection;
|
|
336
332
|
const clientDisconnected = connection.clientDisconnected;
|
|
337
|
-
// Check if this is a "close" event - this is when we should clean up
|
|
338
333
|
const isCloseEvent = data.event === 'close';
|
|
334
|
+
console.log(`[SSE Debug] handleSSEMessage: id=${data.id}, event=${data.event}, clientDisconnected=${clientDisconnected}, isClose=${isCloseEvent}`);
|
|
339
335
|
if (isCloseEvent) {
|
|
340
|
-
console.log(`[SSE E2E] Server: Received close event for ${data.id}, cleaning up`);
|
|
341
|
-
// Now we can safely send sse_close to tunnel client
|
|
342
336
|
const tunnel = this.tunnels.get(tunnelId);
|
|
343
337
|
if (tunnel) {
|
|
344
|
-
tunnel.ws.send(JSON.stringify({
|
|
345
|
-
type: "sse_close",
|
|
346
|
-
id: data.id
|
|
347
|
-
}));
|
|
348
|
-
console.log(`[SSE E2E] Server→Client: Sent sse_close command for ${data.id}`);
|
|
338
|
+
tunnel.ws.send(JSON.stringify({ type: "sse_close", id: data.id }));
|
|
349
339
|
}
|
|
350
|
-
// End the response if client is still connected
|
|
351
340
|
if (!clientDisconnected && res.writable) {
|
|
352
341
|
try {
|
|
353
|
-
res.write(`event: ${data.event}\n`);
|
|
354
|
-
res.write(`data: ${data.data}\n`);
|
|
355
|
-
res.write('\n');
|
|
342
|
+
res.write(`event: ${data.event}\ndata: ${data.data}\n\n`);
|
|
356
343
|
flushSSE(res);
|
|
357
344
|
res.end();
|
|
358
|
-
console.log(`[SSE E2E] Server→Browser: Sent close event and ended response for ${data.id}`);
|
|
359
|
-
}
|
|
360
|
-
catch (error) {
|
|
361
|
-
console.error(`[SSE E2E] Server: ❌ Error writing close event for ${data.id}:`, error);
|
|
362
345
|
}
|
|
346
|
+
catch (error) { /* ignore */ }
|
|
363
347
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
348
|
+
// Decrement request counter for backpressure tracking
|
|
349
|
+
const currentCount = this.tunnelRequestCount.get(tunnelId) || 1;
|
|
350
|
+
this.tunnelRequestCount.set(tunnelId, Math.max(0, currentCount - 1));
|
|
367
351
|
this.sseConnections.delete(data.id);
|
|
368
352
|
return;
|
|
369
353
|
}
|
|
370
|
-
|
|
371
|
-
if (clientDisconnected) {
|
|
372
|
-
// Only log first discard and key events to avoid spam
|
|
373
|
-
if (!(connection.discardCount)) {
|
|
374
|
-
connection.discardCount = 0;
|
|
375
|
-
}
|
|
376
|
-
connection.discardCount++;
|
|
377
|
-
if (connection.discardCount === 1 || data.event === 'error' || data.event === 'end') {
|
|
378
|
-
console.log(`[SSE E2E] Server: Discarding event="${data.event}" for ${data.id} - browser disconnected (total discarded: ${connection.discardCount})`);
|
|
379
|
-
}
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
|
-
// Check if response is still writable before attempting to write
|
|
383
|
-
if (!res.writable) {
|
|
384
|
-
console.log(`[SSE E2E] Server: Response no longer writable for ${data.id}, marking as disconnected`);
|
|
354
|
+
if (clientDisconnected || !res.writable) {
|
|
385
355
|
connection.clientDisconnected = true;
|
|
386
356
|
return;
|
|
387
357
|
}
|
|
388
358
|
try {
|
|
389
|
-
if (data.event)
|
|
359
|
+
if (data.event)
|
|
390
360
|
res.write(`event: ${data.event}\n`);
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
res.write(`data: ${data.data}\n`);
|
|
394
|
-
res.write('\n');
|
|
395
|
-
// Force immediate flush to prevent buffering
|
|
396
|
-
// This is critical for SSE - without it, events may be delayed or lost
|
|
361
|
+
const escapedData = data.data.replace(/\n/g, '\ndata: ');
|
|
362
|
+
res.write(`data: ${escapedData}\n\n`);
|
|
397
363
|
flushSSE(res);
|
|
398
|
-
// Only log key events to avoid spam
|
|
399
|
-
if (!(connection.eventCount)) {
|
|
400
|
-
connection.eventCount = 0;
|
|
401
|
-
}
|
|
402
|
-
connection.eventCount++;
|
|
403
|
-
if (connection.eventCount === 1 || data.event === 'error' || data.event === 'end') {
|
|
404
|
-
console.log(`[SSE E2E] Server→Browser: Forwarded event="${data.event}" for ${data.id} (total events: ${connection.eventCount})`);
|
|
405
|
-
}
|
|
406
364
|
}
|
|
407
365
|
catch (error) {
|
|
408
|
-
console.error(`[SSE E2E] Server: ❌ Error writing SSE message for ${data.id}:`, error);
|
|
409
366
|
connection.clientDisconnected = true;
|
|
410
367
|
}
|
|
411
368
|
}
|
|
412
369
|
handleWebSocketMessage(data) {
|
|
413
370
|
const connection = this.wsConnections.get(data.id);
|
|
414
|
-
if (!connection)
|
|
415
|
-
console.log(`WebSocket connection not found: ${data.id}`);
|
|
371
|
+
if (!connection)
|
|
416
372
|
return;
|
|
417
|
-
}
|
|
418
373
|
const { clientSocket } = connection;
|
|
419
374
|
if (data.event === 'message' && data.data) {
|
|
420
375
|
const messageData = Buffer.from(data.data, 'base64');
|
|
@@ -447,41 +402,57 @@ class DainTunnelServer {
|
|
|
447
402
|
while (retries > 0 && !tunnel) {
|
|
448
403
|
tunnel = this.tunnels.get(tunnelId);
|
|
449
404
|
if (!tunnel) {
|
|
450
|
-
|
|
451
|
-
console.log(`Current tunnels: ${Array.from(this.tunnels.keys()).join(', ')}`);
|
|
452
|
-
await new Promise(resolve => setTimeout(resolve, 100)); // Wait 100ms before retrying
|
|
405
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
453
406
|
retries--;
|
|
454
407
|
}
|
|
455
408
|
}
|
|
456
409
|
if (!tunnel) {
|
|
457
|
-
console.log(`Tunnel not found after retries: ${tunnelId}`);
|
|
458
410
|
return res.status(404).send("Tunnel not found");
|
|
459
411
|
}
|
|
412
|
+
// High-frequency optimization: Check WebSocket buffer and apply backpressure
|
|
413
|
+
if (tunnel.ws.bufferedAmount > 1024 * 1024) {
|
|
414
|
+
return res.status(503).json({ error: "Service Unavailable", message: "Tunnel high load" });
|
|
415
|
+
}
|
|
416
|
+
// Check concurrent request limit per tunnel
|
|
417
|
+
const currentCount = this.tunnelRequestCount.get(tunnelId) || 0;
|
|
418
|
+
if (currentCount >= this.MAX_CONCURRENT_REQUESTS_PER_TUNNEL) {
|
|
419
|
+
return res.status(503).json({ error: "Service Unavailable", message: "Too many concurrent requests" });
|
|
420
|
+
}
|
|
421
|
+
this.tunnelRequestCount.set(tunnelId, currentCount + 1);
|
|
460
422
|
// Check for SSE request
|
|
461
423
|
if (req.headers.accept && req.headers.accept.includes('text/event-stream')) {
|
|
462
424
|
return this.handleSSERequest(req, res, tunnelId, tunnel);
|
|
463
425
|
}
|
|
464
426
|
// Handle regular HTTP request
|
|
465
|
-
const requestId = (
|
|
427
|
+
const requestId = fastId();
|
|
466
428
|
const startTime = Date.now();
|
|
467
|
-
|
|
468
|
-
const
|
|
429
|
+
// Set a timeout for the request (30 seconds)
|
|
430
|
+
const REQUEST_TIMEOUT = 30000;
|
|
431
|
+
const timeoutId = setTimeout(() => {
|
|
432
|
+
const pendingRequest = this.pendingRequests.get(requestId);
|
|
433
|
+
if (pendingRequest) {
|
|
434
|
+
const currentCount = this.tunnelRequestCount.get(tunnelId) || 1;
|
|
435
|
+
this.tunnelRequestCount.set(tunnelId, Math.max(0, currentCount - 1));
|
|
436
|
+
this.pendingRequests.delete(requestId);
|
|
437
|
+
if (!res.headersSent) {
|
|
438
|
+
res.status(504).json({ error: "Gateway Timeout", message: "Request timed out" });
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}, REQUEST_TIMEOUT);
|
|
442
|
+
this.pendingRequests.set(requestId, { res, startTime, tunnelId, timeoutId });
|
|
443
|
+
tunnel.ws.send(JSON.stringify({
|
|
469
444
|
type: "request",
|
|
470
445
|
id: requestId,
|
|
471
446
|
method: req.method,
|
|
472
447
|
path: req.url,
|
|
473
448
|
headers: req.headers,
|
|
474
|
-
body: req.method !== "GET" && req.body
|
|
449
|
+
body: req.method !== "GET" && req.method !== "HEAD" && req.body
|
|
475
450
|
? req.body.toString("base64")
|
|
476
451
|
: undefined,
|
|
477
|
-
};
|
|
478
|
-
tunnel.ws.send(JSON.stringify(requestMessage));
|
|
479
|
-
console.log(`Request forwarded: ${requestId}, Method: ${req.method}, Path: ${req.url}`);
|
|
452
|
+
}));
|
|
480
453
|
}
|
|
481
454
|
handleSSERequest(req, res, tunnelId, tunnel) {
|
|
482
|
-
|
|
483
|
-
const sseId = (0, uuid_1.v4)();
|
|
484
|
-
console.log(`[SSE E2E] Server: Establishing SSE connection ${sseId} from browser to ${req.url}`);
|
|
455
|
+
const sseId = fastId();
|
|
485
456
|
// Optimize TCP socket for low-latency streaming
|
|
486
457
|
const socket = req.socket || req.connection;
|
|
487
458
|
if (socket && socket.setNoDelay) {
|
|
@@ -497,113 +468,114 @@ class DainTunnelServer {
|
|
|
497
468
|
// Flush headers immediately to establish SSE connection
|
|
498
469
|
// Without this, headers may be buffered, delaying connection setup
|
|
499
470
|
res.flushHeaders();
|
|
500
|
-
// Send
|
|
501
|
-
|
|
471
|
+
// Send SSE comment to keep connection alive and verify it's working
|
|
472
|
+
// Comments start with : and are ignored by EventSource but flush the buffer
|
|
473
|
+
res.write(': connected\n\n');
|
|
502
474
|
// Flush the initial write
|
|
503
475
|
flushSSE(res);
|
|
504
|
-
|
|
505
|
-
this.sseConnections.set(sseId, {
|
|
506
|
-
req,
|
|
507
|
-
res,
|
|
508
|
-
id: sseId,
|
|
509
|
-
tunnelId
|
|
510
|
-
});
|
|
511
|
-
console.log(`[SSE E2E] Server→Client: Forwarding SSE connection request ${sseId} to tunnel client`);
|
|
512
|
-
// Notify the tunnel client about the new SSE connection
|
|
476
|
+
this.sseConnections.set(sseId, { req, res, id: sseId, tunnelId });
|
|
513
477
|
tunnel.ws.send(JSON.stringify({
|
|
514
478
|
type: "sse_connection",
|
|
515
479
|
id: sseId,
|
|
516
480
|
path: req.url,
|
|
517
|
-
method: req.method,
|
|
481
|
+
method: req.method,
|
|
518
482
|
headers: req.headers,
|
|
519
483
|
body: req.method !== "GET" && req.body ? req.body.toString("base64") : undefined
|
|
520
484
|
}));
|
|
521
|
-
// Handle client disconnect
|
|
522
485
|
req.on('close', () => {
|
|
523
|
-
console.log(`[SSE
|
|
524
|
-
// DO NOT immediately send sse_close to tunnel client!
|
|
525
|
-
// The local service may still be processing and sending SSE events.
|
|
526
|
-
// Instead, mark this connection as "client disconnected" and let the
|
|
527
|
-
// local service finish naturally. We'll clean up when we receive the
|
|
528
|
-
// final "close" event from the service, or when the connection errors.
|
|
486
|
+
console.log(`[SSE Debug] req.close fired for ${sseId} - client disconnected`);
|
|
529
487
|
const connection = this.sseConnections.get(sseId);
|
|
530
488
|
if (connection) {
|
|
531
|
-
// Mark as client disconnected, but keep the tunnel client connection alive
|
|
532
489
|
connection.clientDisconnected = true;
|
|
533
|
-
|
|
490
|
+
// Clean up after a short delay to allow any pending events to be handled
|
|
491
|
+
setTimeout(() => {
|
|
492
|
+
if (this.sseConnections.has(sseId)) {
|
|
493
|
+
console.log(`[SSE Debug] Cleaning up SSE connection ${sseId} after client disconnect`);
|
|
494
|
+
// Decrement request counter for backpressure tracking
|
|
495
|
+
const currentCount = this.tunnelRequestCount.get(tunnelId) || 1;
|
|
496
|
+
this.tunnelRequestCount.set(tunnelId, Math.max(0, currentCount - 1));
|
|
497
|
+
this.sseConnections.delete(sseId);
|
|
498
|
+
// Notify tunnel client to close the connection
|
|
499
|
+
const tunnel = this.tunnels.get(tunnelId);
|
|
500
|
+
if (tunnel && tunnel.ws.readyState === ws_1.default.OPEN) {
|
|
501
|
+
tunnel.ws.send(JSON.stringify({ type: "sse_close", id: sseId }));
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}, 100);
|
|
534
505
|
}
|
|
535
506
|
});
|
|
507
|
+
req.on('error', (err) => {
|
|
508
|
+
console.log(`[SSE Debug] req.error fired for ${sseId}:`, err.message);
|
|
509
|
+
});
|
|
536
510
|
}
|
|
537
511
|
removeTunnel(ws) {
|
|
538
512
|
try {
|
|
539
|
-
|
|
540
|
-
if (ws.keepAliveInterval) {
|
|
513
|
+
if (ws.keepAliveInterval)
|
|
541
514
|
clearInterval(ws.keepAliveInterval);
|
|
542
|
-
}
|
|
543
|
-
// If we saved tunnelId on the WebSocket object, use it for faster lookup
|
|
544
515
|
const tunnelId = ws.tunnelId;
|
|
545
516
|
let removedTunnelId = tunnelId;
|
|
546
517
|
if (tunnelId && this.tunnels.has(tunnelId)) {
|
|
547
518
|
const tunnel = this.tunnels.get(tunnelId);
|
|
548
|
-
if (tunnel && tunnel.ws === ws)
|
|
519
|
+
if (tunnel && tunnel.ws === ws)
|
|
549
520
|
this.tunnels.delete(tunnelId);
|
|
550
|
-
console.log(`Tunnel removed using stored ID: ${tunnelId}`);
|
|
551
|
-
}
|
|
552
521
|
}
|
|
553
522
|
else {
|
|
554
|
-
// Fall back to iterating through all tunnels
|
|
555
523
|
removedTunnelId = undefined;
|
|
556
524
|
for (const [id, tunnel] of this.tunnels.entries()) {
|
|
557
525
|
if (tunnel.ws === ws) {
|
|
558
526
|
this.tunnels.delete(id);
|
|
559
527
|
removedTunnelId = id;
|
|
560
|
-
console.log(`Tunnel removed: ${id}`);
|
|
561
528
|
break;
|
|
562
529
|
}
|
|
563
530
|
}
|
|
564
531
|
}
|
|
565
532
|
if (removedTunnelId) {
|
|
566
|
-
|
|
533
|
+
this.tunnelRequestCount.delete(removedTunnelId);
|
|
534
|
+
// Close all pending HTTP requests associated with this tunnel
|
|
535
|
+
for (const [requestId, pendingRequest] of this.pendingRequests.entries()) {
|
|
536
|
+
if (pendingRequest.tunnelId === removedTunnelId) {
|
|
537
|
+
try {
|
|
538
|
+
if (pendingRequest.timeoutId)
|
|
539
|
+
clearTimeout(pendingRequest.timeoutId);
|
|
540
|
+
if (!pendingRequest.res.headersSent) {
|
|
541
|
+
pendingRequest.res.status(502).json({ error: "Bad Gateway", message: "Tunnel closed" });
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
catch (error) { /* ignore */ }
|
|
545
|
+
this.pendingRequests.delete(requestId);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
567
548
|
// Close all SSE connections associated with this tunnel
|
|
568
549
|
for (const [sseId, sseConnection] of this.sseConnections.entries()) {
|
|
569
550
|
if (sseConnection.tunnelId === removedTunnelId) {
|
|
570
551
|
try {
|
|
571
552
|
sseConnection.res.end();
|
|
572
553
|
}
|
|
573
|
-
catch (error) {
|
|
574
|
-
console.error(`Error closing SSE connection ${sseId}:`, error);
|
|
575
|
-
}
|
|
554
|
+
catch (error) { /* ignore */ }
|
|
576
555
|
this.sseConnections.delete(sseId);
|
|
577
556
|
}
|
|
578
557
|
}
|
|
579
558
|
// Close all WebSocket connections associated with this tunnel
|
|
580
559
|
for (const [wsId, wsConnection] of this.wsConnections.entries()) {
|
|
581
|
-
|
|
582
|
-
if (wsPath.startsWith(`/${removedTunnelId}/`)) {
|
|
560
|
+
if (wsConnection.path.startsWith(`/${removedTunnelId}/`)) {
|
|
583
561
|
try {
|
|
584
562
|
wsConnection.clientSocket.close(1001, "Tunnel closed");
|
|
585
563
|
}
|
|
586
|
-
catch (error) {
|
|
587
|
-
console.error(`Error closing WebSocket connection ${wsId}:`, error);
|
|
588
|
-
}
|
|
564
|
+
catch (error) { /* ignore */ }
|
|
589
565
|
this.wsConnections.delete(wsId);
|
|
590
566
|
}
|
|
591
567
|
}
|
|
592
568
|
}
|
|
593
|
-
|
|
594
|
-
console.log(`No tunnel found to remove for the closed WebSocket connection`);
|
|
595
|
-
}
|
|
596
|
-
// Also remove any pending challenges for this WebSocket
|
|
569
|
+
// Remove any pending challenges for this WebSocket
|
|
597
570
|
for (const [challenge, challengeObj] of this.challenges.entries()) {
|
|
598
571
|
if (challengeObj.ws === ws) {
|
|
599
572
|
this.challenges.delete(challenge);
|
|
600
|
-
console.log(`Challenge removed for closed WebSocket`);
|
|
601
573
|
break;
|
|
602
574
|
}
|
|
603
575
|
}
|
|
604
576
|
}
|
|
605
577
|
catch (error) {
|
|
606
|
-
console.error("
|
|
578
|
+
console.error("[Tunnel] Remove error:", error);
|
|
607
579
|
}
|
|
608
580
|
}
|
|
609
581
|
async start() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dainprotocol/tunnel",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.17",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"private": false,
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"author": "Ryan",
|
|
20
20
|
"license": "ISC",
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@dainprotocol/service-sdk": "
|
|
22
|
+
"@dainprotocol/service-sdk": "2.0.56",
|
|
23
23
|
"@types/body-parser": "^1.19.5",
|
|
24
24
|
"@types/cors": "^2.8.17",
|
|
25
25
|
"@types/eventsource": "^3.0.0",
|
|
@@ -77,4 +77,4 @@
|
|
|
77
77
|
]
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
|
-
}
|
|
80
|
+
}
|