@dainprotocol/tunnel 1.1.31 → 1.1.35
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 +10 -0
- package/dist/client/index.js +132 -21
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +94 -14
- package/package.json +2 -2
package/dist/client/index.d.ts
CHANGED
|
@@ -9,9 +9,12 @@ declare class DainTunnel extends EventEmitter {
|
|
|
9
9
|
private tunnelId;
|
|
10
10
|
private secret;
|
|
11
11
|
private webSocketClients;
|
|
12
|
+
private pendingWebSocketMessages;
|
|
12
13
|
private sseClients;
|
|
13
14
|
private httpAgent;
|
|
14
15
|
private heartbeatInterval;
|
|
16
|
+
private reconnectTimer;
|
|
17
|
+
private isStopping;
|
|
15
18
|
constructor(serverUrl: string, apiKey: string);
|
|
16
19
|
private signChallenge;
|
|
17
20
|
private safeSend;
|
|
@@ -25,10 +28,17 @@ declare class DainTunnel extends EventEmitter {
|
|
|
25
28
|
private handleRequest;
|
|
26
29
|
private handleWebSocketConnection;
|
|
27
30
|
private handleWebSocketMessage;
|
|
31
|
+
private flushPendingWebSocketMessages;
|
|
32
|
+
private cleanupWebSocketConnection;
|
|
28
33
|
private handleSSEConnection;
|
|
29
34
|
private handleSSEClose;
|
|
30
35
|
private forwardRequest;
|
|
31
36
|
private attemptReconnect;
|
|
37
|
+
/**
|
|
38
|
+
* Reset reconnection attempts counter.
|
|
39
|
+
* Call this to allow reconnection after max_reconnect_attempts was reached.
|
|
40
|
+
*/
|
|
41
|
+
resetReconnection(): void;
|
|
32
42
|
stop(): Promise<void>;
|
|
33
43
|
}
|
|
34
44
|
export { DainTunnel };
|
package/dist/client/index.js
CHANGED
|
@@ -14,12 +14,35 @@ const TIMEOUTS = {
|
|
|
14
14
|
REQUEST: 25000,
|
|
15
15
|
CONNECTION: 10000,
|
|
16
16
|
CHALLENGE: 5000,
|
|
17
|
-
RECONNECT_DELAY: 5000,
|
|
18
17
|
SHUTDOWN_GRACE: 500,
|
|
19
18
|
};
|
|
20
19
|
const RECONNECT = {
|
|
21
|
-
MAX_ATTEMPTS: 5
|
|
20
|
+
MAX_ATTEMPTS: 10, // Increase from 5
|
|
21
|
+
BASE_DELAY: 1000, // Start at 1s
|
|
22
|
+
MAX_DELAY: 30000, // Cap at 30s
|
|
23
|
+
JITTER: 0.3, // 30% randomization
|
|
22
24
|
};
|
|
25
|
+
const LIMITS = {
|
|
26
|
+
MAX_PENDING_WS_MESSAGES_PER_CONNECTION: 256,
|
|
27
|
+
};
|
|
28
|
+
function rawDataToString(message) {
|
|
29
|
+
if (typeof message === "string")
|
|
30
|
+
return message;
|
|
31
|
+
if (Buffer.isBuffer(message))
|
|
32
|
+
return message.toString("utf8");
|
|
33
|
+
if (Array.isArray(message))
|
|
34
|
+
return Buffer.concat(message).toString("utf8");
|
|
35
|
+
return Buffer.from(message).toString("utf8");
|
|
36
|
+
}
|
|
37
|
+
function rawDataToBuffer(data) {
|
|
38
|
+
if (Buffer.isBuffer(data))
|
|
39
|
+
return data;
|
|
40
|
+
if (Array.isArray(data))
|
|
41
|
+
return Buffer.concat(data);
|
|
42
|
+
if (typeof data === "string")
|
|
43
|
+
return Buffer.from(data, "utf8");
|
|
44
|
+
return Buffer.from(data);
|
|
45
|
+
}
|
|
23
46
|
class DainTunnel extends events_1.EventEmitter {
|
|
24
47
|
constructor(serverUrl, apiKey) {
|
|
25
48
|
super();
|
|
@@ -29,8 +52,11 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
29
52
|
this.port = null;
|
|
30
53
|
this.reconnectAttempts = 0;
|
|
31
54
|
this.webSocketClients = new Map();
|
|
55
|
+
this.pendingWebSocketMessages = new Map();
|
|
32
56
|
this.sseClients = new Map();
|
|
33
57
|
this.heartbeatInterval = null;
|
|
58
|
+
this.reconnectTimer = null;
|
|
59
|
+
this.isStopping = false;
|
|
34
60
|
const parsed = (0, auth_1.parseAPIKey)(apiKey);
|
|
35
61
|
if (!parsed) {
|
|
36
62
|
throw new Error('Invalid API key format. Expected: sk_agent_{agentId}_{orgId}_{secret}');
|
|
@@ -82,6 +108,7 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
82
108
|
}
|
|
83
109
|
}
|
|
84
110
|
async start(port) {
|
|
111
|
+
this.isStopping = false;
|
|
85
112
|
this.port = port;
|
|
86
113
|
return this.connect();
|
|
87
114
|
}
|
|
@@ -128,7 +155,7 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
128
155
|
});
|
|
129
156
|
this.ws.on("message", (data) => {
|
|
130
157
|
try {
|
|
131
|
-
this.handleMessage(JSON.parse(data), (url) => finish(url));
|
|
158
|
+
this.handleMessage(JSON.parse(rawDataToString(data)), (url) => finish(url));
|
|
132
159
|
}
|
|
133
160
|
catch (err) {
|
|
134
161
|
finish(undefined, err);
|
|
@@ -137,6 +164,9 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
137
164
|
this.ws.on("close", () => {
|
|
138
165
|
this.stopHeartbeat();
|
|
139
166
|
this.cleanupAllClients();
|
|
167
|
+
this.ws = null;
|
|
168
|
+
if (this.isStopping)
|
|
169
|
+
return;
|
|
140
170
|
if (this.tunnelUrl) {
|
|
141
171
|
this.emit("disconnected");
|
|
142
172
|
this.attemptReconnect();
|
|
@@ -178,6 +208,7 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
178
208
|
catch (_b) { }
|
|
179
209
|
}
|
|
180
210
|
this.webSocketClients.clear();
|
|
211
|
+
this.pendingWebSocketMessages.clear();
|
|
181
212
|
}
|
|
182
213
|
async requestChallenge() {
|
|
183
214
|
return new Promise((resolve, reject) => {
|
|
@@ -206,7 +237,7 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
206
237
|
};
|
|
207
238
|
const challengeHandler = (message) => {
|
|
208
239
|
try {
|
|
209
|
-
const data = JSON.parse(message);
|
|
240
|
+
const data = JSON.parse(rawDataToString(message));
|
|
210
241
|
if (data.type === "challenge") {
|
|
211
242
|
finish(data.challenge);
|
|
212
243
|
}
|
|
@@ -281,17 +312,21 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
281
312
|
const client = new ws_1.default(`ws://localhost:${this.port}${message.path}`, {
|
|
282
313
|
headers
|
|
283
314
|
});
|
|
315
|
+
this.pendingWebSocketMessages.set(message.id, []);
|
|
284
316
|
this.webSocketClients.set(message.id, client);
|
|
317
|
+
client.on('open', () => {
|
|
318
|
+
this.flushPendingWebSocketMessages(message.id, sendWsEvent);
|
|
319
|
+
});
|
|
285
320
|
client.on('message', (data) => {
|
|
286
|
-
sendWsEvent('message', data.toString('base64'));
|
|
321
|
+
sendWsEvent('message', rawDataToBuffer(data).toString('base64'));
|
|
287
322
|
});
|
|
288
323
|
client.on('close', () => {
|
|
289
324
|
sendWsEvent('close');
|
|
290
|
-
this.
|
|
325
|
+
this.cleanupWebSocketConnection(message.id);
|
|
291
326
|
});
|
|
292
327
|
client.on('error', (error) => {
|
|
293
328
|
sendWsEvent('error', error.message);
|
|
294
|
-
this.
|
|
329
|
+
this.cleanupWebSocketConnection(message.id);
|
|
295
330
|
});
|
|
296
331
|
this.emit("websocket_connection", { id: message.id, path: message.path });
|
|
297
332
|
}
|
|
@@ -305,18 +340,57 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
305
340
|
return;
|
|
306
341
|
switch (message.event) {
|
|
307
342
|
case 'message':
|
|
308
|
-
if (message.data
|
|
343
|
+
if (!message.data)
|
|
344
|
+
return;
|
|
345
|
+
if (client.readyState === ws_1.default.OPEN) {
|
|
309
346
|
client.send(Buffer.from(message.data, 'base64'));
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (client.readyState !== ws_1.default.CONNECTING)
|
|
350
|
+
return;
|
|
351
|
+
const pending = this.pendingWebSocketMessages.get(message.id);
|
|
352
|
+
if (!pending)
|
|
353
|
+
return;
|
|
354
|
+
if (pending.length >= LIMITS.MAX_PENDING_WS_MESSAGES_PER_CONNECTION) {
|
|
355
|
+
client.close(1013, "WebSocket upstream overloaded");
|
|
356
|
+
this.cleanupWebSocketConnection(message.id);
|
|
357
|
+
return;
|
|
310
358
|
}
|
|
359
|
+
pending.push(Buffer.from(message.data, 'base64'));
|
|
311
360
|
break;
|
|
312
361
|
case 'close':
|
|
313
|
-
if (client.readyState === ws_1.default.OPEN) {
|
|
362
|
+
if (client.readyState === ws_1.default.OPEN || client.readyState === ws_1.default.CONNECTING) {
|
|
314
363
|
client.close();
|
|
315
364
|
}
|
|
316
|
-
this.
|
|
365
|
+
this.cleanupWebSocketConnection(message.id);
|
|
366
|
+
break;
|
|
367
|
+
case 'error':
|
|
368
|
+
if (client.readyState === ws_1.default.OPEN || client.readyState === ws_1.default.CONNECTING) {
|
|
369
|
+
client.close(1011, message.data);
|
|
370
|
+
}
|
|
371
|
+
this.cleanupWebSocketConnection(message.id);
|
|
317
372
|
break;
|
|
318
373
|
}
|
|
319
374
|
}
|
|
375
|
+
flushPendingWebSocketMessages(id, sendWsEvent) {
|
|
376
|
+
const client = this.webSocketClients.get(id);
|
|
377
|
+
const pending = this.pendingWebSocketMessages.get(id);
|
|
378
|
+
if (!client || !pending || pending.length === 0)
|
|
379
|
+
return;
|
|
380
|
+
try {
|
|
381
|
+
while (pending.length > 0 && client.readyState === ws_1.default.OPEN) {
|
|
382
|
+
client.send(pending.shift());
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
sendWsEvent("error", error.message);
|
|
387
|
+
this.cleanupWebSocketConnection(id);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
cleanupWebSocketConnection(id) {
|
|
391
|
+
this.pendingWebSocketMessages.delete(id);
|
|
392
|
+
this.webSocketClients.delete(id);
|
|
393
|
+
}
|
|
320
394
|
handleSSEConnection(message) {
|
|
321
395
|
const sendSseEvent = (event, data = '') => {
|
|
322
396
|
this.safeSend({ type: 'sse', id: message.id, event, data });
|
|
@@ -339,12 +413,17 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
339
413
|
headers,
|
|
340
414
|
agent: this.httpAgent,
|
|
341
415
|
}, (res) => {
|
|
416
|
+
const cleanup = () => {
|
|
417
|
+
this.sseClients.delete(message.id);
|
|
418
|
+
};
|
|
342
419
|
if (res.statusCode !== 200) {
|
|
343
420
|
let errorBody = '';
|
|
344
421
|
res.on('data', (chunk) => { errorBody += chunk.toString(); });
|
|
345
422
|
res.on('end', () => {
|
|
346
423
|
sendSseEvent('error', `Status ${res.statusCode}: ${errorBody.substring(0, 200)}`);
|
|
424
|
+
cleanup();
|
|
347
425
|
});
|
|
426
|
+
res.on('close', cleanup);
|
|
348
427
|
return;
|
|
349
428
|
}
|
|
350
429
|
const socket = res.socket || res.connection;
|
|
@@ -390,6 +469,12 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
390
469
|
processBuffer();
|
|
391
470
|
}
|
|
392
471
|
sendSseEvent('close');
|
|
472
|
+
cleanup();
|
|
473
|
+
});
|
|
474
|
+
res.on('close', cleanup);
|
|
475
|
+
res.on('error', (error) => {
|
|
476
|
+
sendSseEvent('error', error.message);
|
|
477
|
+
cleanup();
|
|
393
478
|
});
|
|
394
479
|
});
|
|
395
480
|
req.on('socket', (socket) => {
|
|
@@ -398,12 +483,16 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
398
483
|
});
|
|
399
484
|
req.on('error', (error) => {
|
|
400
485
|
sendSseEvent('error', error.message);
|
|
486
|
+
this.sseClients.delete(message.id);
|
|
487
|
+
});
|
|
488
|
+
req.on('close', () => {
|
|
489
|
+
this.sseClients.delete(message.id);
|
|
401
490
|
});
|
|
491
|
+
this.sseClients.set(message.id, req);
|
|
402
492
|
if (message.body && message.method !== 'GET') {
|
|
403
493
|
req.write(Buffer.from(message.body, 'base64'));
|
|
404
494
|
}
|
|
405
495
|
req.end();
|
|
406
|
-
this.sseClients.set(message.id, req);
|
|
407
496
|
this.emit("sse_connection", { id: message.id, path: message.path });
|
|
408
497
|
}
|
|
409
498
|
catch (error) {
|
|
@@ -486,22 +575,44 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
486
575
|
});
|
|
487
576
|
}
|
|
488
577
|
attemptReconnect() {
|
|
489
|
-
if (this.
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
try {
|
|
493
|
-
await this.connect();
|
|
494
|
-
}
|
|
495
|
-
catch (_a) { }
|
|
496
|
-
}, TIMEOUTS.RECONNECT_DELAY);
|
|
497
|
-
}
|
|
498
|
-
else {
|
|
578
|
+
if (this.isStopping)
|
|
579
|
+
return;
|
|
580
|
+
if (this.reconnectAttempts >= RECONNECT.MAX_ATTEMPTS) {
|
|
499
581
|
this.emit("max_reconnect_attempts");
|
|
582
|
+
return;
|
|
500
583
|
}
|
|
584
|
+
this.reconnectAttempts++;
|
|
585
|
+
// Exponential backoff with jitter
|
|
586
|
+
const baseDelay = Math.min(RECONNECT.BASE_DELAY * Math.pow(2, this.reconnectAttempts - 1), RECONNECT.MAX_DELAY);
|
|
587
|
+
const jitter = baseDelay * RECONNECT.JITTER * (Math.random() - 0.5);
|
|
588
|
+
const delay = Math.round(baseDelay + jitter);
|
|
589
|
+
this.emit("reconnecting", { attempt: this.reconnectAttempts, delay });
|
|
590
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
591
|
+
try {
|
|
592
|
+
await this.connect();
|
|
593
|
+
this.emit("reconnected");
|
|
594
|
+
}
|
|
595
|
+
catch (_a) {
|
|
596
|
+
if (!this.isStopping)
|
|
597
|
+
this.attemptReconnect();
|
|
598
|
+
}
|
|
599
|
+
}, delay);
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Reset reconnection attempts counter.
|
|
603
|
+
* Call this to allow reconnection after max_reconnect_attempts was reached.
|
|
604
|
+
*/
|
|
605
|
+
resetReconnection() {
|
|
606
|
+
this.reconnectAttempts = 0;
|
|
501
607
|
}
|
|
502
608
|
async stop() {
|
|
609
|
+
this.isStopping = true;
|
|
503
610
|
this.stopHeartbeat();
|
|
504
611
|
this.cleanupAllClients();
|
|
612
|
+
if (this.reconnectTimer) {
|
|
613
|
+
clearTimeout(this.reconnectTimer);
|
|
614
|
+
this.reconnectTimer = null;
|
|
615
|
+
}
|
|
505
616
|
if (this.ws) {
|
|
506
617
|
try {
|
|
507
618
|
if (this.ws.readyState === ws_1.default.OPEN)
|
package/dist/server/index.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ declare class DainTunnelServer {
|
|
|
12
12
|
private tunnelRequestCount;
|
|
13
13
|
private safeSend;
|
|
14
14
|
private decrementRequestCount;
|
|
15
|
+
private buildForwardedHeaders;
|
|
16
|
+
private buildTunnelUrl;
|
|
15
17
|
constructor(hostname: string, port: number);
|
|
16
18
|
private setupExpressRoutes;
|
|
17
19
|
private setupWebSocketServer;
|
package/dist/server/index.js
CHANGED
|
@@ -41,6 +41,27 @@ function extractPathParts(url) {
|
|
|
41
41
|
const path = qIdx >= 0 ? url.slice(0, qIdx) : url;
|
|
42
42
|
return path.split('/');
|
|
43
43
|
}
|
|
44
|
+
function rawDataToString(message) {
|
|
45
|
+
if (typeof message === "string")
|
|
46
|
+
return message;
|
|
47
|
+
if (Buffer.isBuffer(message))
|
|
48
|
+
return message.toString("utf8");
|
|
49
|
+
if (Array.isArray(message))
|
|
50
|
+
return Buffer.concat(message).toString("utf8");
|
|
51
|
+
return Buffer.from(message).toString("utf8");
|
|
52
|
+
}
|
|
53
|
+
function rawDataToBase64(data) {
|
|
54
|
+
if (typeof data === "string")
|
|
55
|
+
return Buffer.from(data, "utf8").toString("base64");
|
|
56
|
+
if (Buffer.isBuffer(data))
|
|
57
|
+
return data.toString("base64");
|
|
58
|
+
if (Array.isArray(data))
|
|
59
|
+
return Buffer.concat(data).toString("base64");
|
|
60
|
+
return Buffer.from(data).toString("base64");
|
|
61
|
+
}
|
|
62
|
+
function normalizeHostToBaseUrl(hostname) {
|
|
63
|
+
return /^https?:\/\//i.test(hostname) ? hostname : `http://${hostname}`;
|
|
64
|
+
}
|
|
44
65
|
class DainTunnelServer {
|
|
45
66
|
safeSend(ws, data) {
|
|
46
67
|
try {
|
|
@@ -55,8 +76,29 @@ class DainTunnelServer {
|
|
|
55
76
|
return false;
|
|
56
77
|
}
|
|
57
78
|
decrementRequestCount(tunnelId) {
|
|
58
|
-
const currentCount = this.tunnelRequestCount.get(tunnelId)
|
|
59
|
-
|
|
79
|
+
const currentCount = this.tunnelRequestCount.get(tunnelId);
|
|
80
|
+
if (!currentCount)
|
|
81
|
+
return;
|
|
82
|
+
if (currentCount <= 1) {
|
|
83
|
+
this.tunnelRequestCount.delete(tunnelId);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
this.tunnelRequestCount.set(tunnelId, currentCount - 1);
|
|
87
|
+
}
|
|
88
|
+
buildForwardedHeaders(headers, tunnelId) {
|
|
89
|
+
const forwardedHeaders = { ...headers };
|
|
90
|
+
if (!forwardedHeaders["x-forwarded-prefix"]) {
|
|
91
|
+
forwardedHeaders["x-forwarded-prefix"] = `/${tunnelId}`;
|
|
92
|
+
}
|
|
93
|
+
return forwardedHeaders;
|
|
94
|
+
}
|
|
95
|
+
buildTunnelUrl(tunnelId) {
|
|
96
|
+
const baseUrl = normalizeHostToBaseUrl(this.hostname);
|
|
97
|
+
const url = new URL(baseUrl);
|
|
98
|
+
if (process.env.SKIP_PORT !== "true" && !url.port) {
|
|
99
|
+
url.port = String(this.port);
|
|
100
|
+
}
|
|
101
|
+
return `${url.toString().replace(/\/$/, "")}/${tunnelId}`;
|
|
60
102
|
}
|
|
61
103
|
constructor(hostname, port) {
|
|
62
104
|
this.hostname = hostname;
|
|
@@ -149,7 +191,7 @@ class DainTunnelServer {
|
|
|
149
191
|
handleTunnelClientConnection(ws, req) {
|
|
150
192
|
ws.on("message", (message) => {
|
|
151
193
|
try {
|
|
152
|
-
const data = JSON.parse(message);
|
|
194
|
+
const data = JSON.parse(rawDataToString(message));
|
|
153
195
|
switch (data.type) {
|
|
154
196
|
case "challenge_request":
|
|
155
197
|
this.handleChallengeRequest(ws);
|
|
@@ -190,18 +232,19 @@ class DainTunnelServer {
|
|
|
190
232
|
return;
|
|
191
233
|
}
|
|
192
234
|
const wsConnectionId = fastId();
|
|
235
|
+
const forwardedHeaders = this.buildForwardedHeaders(req.headers, tunnelId);
|
|
193
236
|
this.wsConnections.set(wsConnectionId, {
|
|
194
237
|
clientSocket: ws,
|
|
195
238
|
id: wsConnectionId,
|
|
196
239
|
path: remainingPath,
|
|
197
|
-
headers:
|
|
240
|
+
headers: forwardedHeaders,
|
|
198
241
|
tunnelId
|
|
199
242
|
});
|
|
200
243
|
this.safeSend(tunnel.ws, {
|
|
201
244
|
type: "websocket_connection",
|
|
202
245
|
id: wsConnectionId,
|
|
203
246
|
path: remainingPath,
|
|
204
|
-
headers:
|
|
247
|
+
headers: forwardedHeaders
|
|
205
248
|
});
|
|
206
249
|
const sendToTunnel = (event, data) => {
|
|
207
250
|
const currentTunnel = this.tunnels.get(tunnelId);
|
|
@@ -209,7 +252,7 @@ class DainTunnelServer {
|
|
|
209
252
|
this.safeSend(currentTunnel.ws, { type: "websocket", id: wsConnectionId, event, data });
|
|
210
253
|
}
|
|
211
254
|
};
|
|
212
|
-
ws.on("message", (data) => sendToTunnel("message", data
|
|
255
|
+
ws.on("message", (data) => sendToTunnel("message", rawDataToBase64(data)));
|
|
213
256
|
ws.on("close", () => { sendToTunnel("close"); this.wsConnections.delete(wsConnectionId); });
|
|
214
257
|
ws.on("error", (error) => sendToTunnel("error", error.message));
|
|
215
258
|
}
|
|
@@ -291,10 +334,7 @@ class DainTunnelServer {
|
|
|
291
334
|
}, TIMEOUTS.PING_INTERVAL);
|
|
292
335
|
ws.keepAliveInterval = intervalId;
|
|
293
336
|
ws.on("close", () => clearInterval(intervalId));
|
|
294
|
-
|
|
295
|
-
if (process.env.SKIP_PORT !== "true")
|
|
296
|
-
tunnelUrl += `:${this.port}`;
|
|
297
|
-
tunnelUrl += `/${tunnelId}`;
|
|
337
|
+
const tunnelUrl = this.buildTunnelUrl(tunnelId);
|
|
298
338
|
ws.send(JSON.stringify({ type: "tunnelUrl", url: tunnelUrl }));
|
|
299
339
|
console.log(`[Tunnel] Created: ${tunnelUrl}`);
|
|
300
340
|
}
|
|
@@ -389,6 +429,13 @@ class DainTunnelServer {
|
|
|
389
429
|
const tunnelId = req.params.tunnelId;
|
|
390
430
|
if (((_a = req.headers.upgrade) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'websocket')
|
|
391
431
|
return;
|
|
432
|
+
if (!tunnelId || !tunnelId.includes("_")) {
|
|
433
|
+
res.status(404).json({
|
|
434
|
+
error: "Not Found",
|
|
435
|
+
message: `Tunnel "${tunnelId}" does not exist.`,
|
|
436
|
+
});
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
392
439
|
let tunnel;
|
|
393
440
|
let retries = LIMITS.TUNNEL_RETRY_COUNT;
|
|
394
441
|
while (retries > 0 && !tunnel) {
|
|
@@ -399,7 +446,6 @@ class DainTunnelServer {
|
|
|
399
446
|
}
|
|
400
447
|
}
|
|
401
448
|
if (!tunnel) {
|
|
402
|
-
this.decrementRequestCount(tunnelId);
|
|
403
449
|
res.status(502).json({
|
|
404
450
|
error: "Bad Gateway",
|
|
405
451
|
message: `Tunnel "${tunnelId}" not connected. The service may be offline or reconnecting.`,
|
|
@@ -441,7 +487,7 @@ class DainTunnelServer {
|
|
|
441
487
|
id: requestId,
|
|
442
488
|
method: req.method,
|
|
443
489
|
path: req.url,
|
|
444
|
-
headers: req.headers,
|
|
490
|
+
headers: this.buildForwardedHeaders(req.headers, tunnelId),
|
|
445
491
|
body: hasBody ? req.body.toString("base64") : undefined,
|
|
446
492
|
});
|
|
447
493
|
if (!sent) {
|
|
@@ -507,7 +553,7 @@ class DainTunnelServer {
|
|
|
507
553
|
id: sseId,
|
|
508
554
|
path: req.url,
|
|
509
555
|
method: req.method,
|
|
510
|
-
headers: req.headers,
|
|
556
|
+
headers: this.buildForwardedHeaders(req.headers, tunnelId),
|
|
511
557
|
body: hasBody ? req.body.toString("base64") : undefined
|
|
512
558
|
});
|
|
513
559
|
if (!sent) {
|
|
@@ -609,7 +655,6 @@ class DainTunnelServer {
|
|
|
609
655
|
for (const [challenge, obj] of this.challenges.entries()) {
|
|
610
656
|
if (obj.ws === ws) {
|
|
611
657
|
this.challenges.delete(challenge);
|
|
612
|
-
break;
|
|
613
658
|
}
|
|
614
659
|
}
|
|
615
660
|
}
|
|
@@ -624,7 +669,33 @@ class DainTunnelServer {
|
|
|
624
669
|
async stop() {
|
|
625
670
|
return new Promise((resolve) => {
|
|
626
671
|
try {
|
|
672
|
+
for (const tunnel of this.tunnels.values()) {
|
|
673
|
+
if (tunnel.ws.keepAliveInterval)
|
|
674
|
+
clearInterval(tunnel.ws.keepAliveInterval);
|
|
675
|
+
try {
|
|
676
|
+
tunnel.ws.terminate();
|
|
677
|
+
}
|
|
678
|
+
catch (error) {
|
|
679
|
+
console.error(`Error closing tunnel ${tunnel.id}:`, error);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
this.tunnels.clear();
|
|
683
|
+
this.tunnelRequestCount.clear();
|
|
684
|
+
for (const [requestId, pending] of this.pendingRequests.entries()) {
|
|
685
|
+
if (pending.timeoutId)
|
|
686
|
+
clearTimeout(pending.timeoutId);
|
|
687
|
+
if (!pending.res.headersSent) {
|
|
688
|
+
try {
|
|
689
|
+
pending.res.status(503).json({ error: "Service Unavailable", message: "Server shutting down" });
|
|
690
|
+
}
|
|
691
|
+
catch (_a) { }
|
|
692
|
+
}
|
|
693
|
+
this.pendingRequests.delete(requestId);
|
|
694
|
+
}
|
|
695
|
+
this.challenges.clear();
|
|
627
696
|
for (const [sseId, conn] of this.sseConnections.entries()) {
|
|
697
|
+
if (conn.keepAliveInterval)
|
|
698
|
+
clearInterval(conn.keepAliveInterval);
|
|
628
699
|
try {
|
|
629
700
|
conn.res.end();
|
|
630
701
|
}
|
|
@@ -642,7 +713,16 @@ class DainTunnelServer {
|
|
|
642
713
|
}
|
|
643
714
|
this.wsConnections.delete(wsId);
|
|
644
715
|
}
|
|
716
|
+
for (const socket of this.wss.clients) {
|
|
717
|
+
try {
|
|
718
|
+
socket.terminate();
|
|
719
|
+
}
|
|
720
|
+
catch (_b) { }
|
|
721
|
+
}
|
|
645
722
|
this.wss.close(() => {
|
|
723
|
+
var _a, _b, _c, _d;
|
|
724
|
+
(_b = (_a = this.server).closeIdleConnections) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
725
|
+
(_d = (_c = this.server).closeAllConnections) === null || _d === void 0 ? void 0 : _d.call(_c);
|
|
646
726
|
this.server.close(() => resolve());
|
|
647
727
|
});
|
|
648
728
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dainprotocol/tunnel",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.35",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"private": false,
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"author": "Ryan",
|
|
21
21
|
"license": "ISC",
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@dainprotocol/service-sdk": "2.0.
|
|
23
|
+
"@dainprotocol/service-sdk": "2.0.82",
|
|
24
24
|
"@types/body-parser": "^1.19.5",
|
|
25
25
|
"@types/cors": "^2.8.17",
|
|
26
26
|
"@types/eventsource": "^3.0.0",
|