@dainprotocol/tunnel 1.0.5 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/index.d.ts +13 -1
- package/dist/client/index.js +339 -42
- package/dist/server/index.d.ts +8 -1
- package/dist/server/index.js +405 -78
- package/dist/server/start.js +1 -0
- package/package.json +5 -4
package/dist/client/index.d.ts
CHANGED
|
@@ -7,14 +7,26 @@ declare class DainTunnel extends EventEmitter {
|
|
|
7
7
|
private reconnectAttempts;
|
|
8
8
|
private maxReconnectAttempts;
|
|
9
9
|
private reconnectDelay;
|
|
10
|
-
private
|
|
10
|
+
private apiKey;
|
|
11
11
|
private tunnelId;
|
|
12
|
+
private secret;
|
|
13
|
+
private webSocketClients;
|
|
14
|
+
private sseClients;
|
|
12
15
|
constructor(serverUrl: string, apiKey: string);
|
|
16
|
+
/**
|
|
17
|
+
* Sign a challenge using HMAC-SHA256
|
|
18
|
+
* @private
|
|
19
|
+
*/
|
|
20
|
+
private signChallenge;
|
|
13
21
|
start(port: number): Promise<string>;
|
|
14
22
|
private connect;
|
|
15
23
|
private requestChallenge;
|
|
16
24
|
private handleMessage;
|
|
17
25
|
private handleRequest;
|
|
26
|
+
private handleWebSocketConnection;
|
|
27
|
+
private handleWebSocketMessage;
|
|
28
|
+
private handleSSEConnection;
|
|
29
|
+
private handleSSEClose;
|
|
18
30
|
private forwardRequest;
|
|
19
31
|
private sendMessage;
|
|
20
32
|
private attemptReconnect;
|
package/dist/client/index.js
CHANGED
|
@@ -7,8 +7,8 @@ exports.DainTunnel = void 0;
|
|
|
7
7
|
const ws_1 = __importDefault(require("ws"));
|
|
8
8
|
const http_1 = __importDefault(require("http"));
|
|
9
9
|
const events_1 = require("events");
|
|
10
|
-
const
|
|
11
|
-
const
|
|
10
|
+
const crypto_1 = require("crypto");
|
|
11
|
+
const auth_1 = require("@dainprotocol/service-sdk/service/auth");
|
|
12
12
|
class DainTunnel extends events_1.EventEmitter {
|
|
13
13
|
constructor(serverUrl, apiKey) {
|
|
14
14
|
super();
|
|
@@ -19,8 +19,25 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
19
19
|
this.reconnectAttempts = 0;
|
|
20
20
|
this.maxReconnectAttempts = 5;
|
|
21
21
|
this.reconnectDelay = 5000;
|
|
22
|
-
this.
|
|
23
|
-
this.
|
|
22
|
+
this.webSocketClients = new Map();
|
|
23
|
+
this.sseClients = new Map();
|
|
24
|
+
// Parse API key to extract agentId and secret
|
|
25
|
+
const parsed = (0, auth_1.parseAPIKey)(apiKey);
|
|
26
|
+
if (!parsed) {
|
|
27
|
+
throw new Error('Invalid API key format. Expected: sk_agent_{agentId}_{orgId}_{secret}');
|
|
28
|
+
}
|
|
29
|
+
this.apiKey = apiKey;
|
|
30
|
+
this.tunnelId = parsed.agentId; // agentId is the tunnel identifier
|
|
31
|
+
this.secret = parsed.secret; // secret for HMAC signatures
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Sign a challenge using HMAC-SHA256
|
|
35
|
+
* @private
|
|
36
|
+
*/
|
|
37
|
+
signChallenge(challenge) {
|
|
38
|
+
return (0, crypto_1.createHmac)('sha256', this.secret)
|
|
39
|
+
.update(challenge)
|
|
40
|
+
.digest('hex');
|
|
24
41
|
}
|
|
25
42
|
async start(port) {
|
|
26
43
|
this.port = port;
|
|
@@ -28,39 +45,68 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
28
45
|
}
|
|
29
46
|
async connect() {
|
|
30
47
|
return new Promise((resolve, reject) => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
this.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
48
|
+
try {
|
|
49
|
+
console.log(`Connecting to WebSocket server: ${this.serverUrl}`);
|
|
50
|
+
this.ws = new ws_1.default(this.serverUrl);
|
|
51
|
+
this.ws.on("open", async () => {
|
|
52
|
+
console.log('WebSocket connection opened');
|
|
53
|
+
this.reconnectAttempts = 0;
|
|
54
|
+
try {
|
|
55
|
+
const challenge = await this.requestChallenge();
|
|
56
|
+
const signature = this.signChallenge(challenge);
|
|
57
|
+
this.sendMessage({
|
|
58
|
+
type: "start",
|
|
59
|
+
port: this.port,
|
|
60
|
+
challenge,
|
|
61
|
+
signature,
|
|
62
|
+
tunnelId: this.tunnelId,
|
|
63
|
+
apiKey: this.apiKey // Send API key for server validation
|
|
64
|
+
});
|
|
65
|
+
this.emit("connected");
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
console.error('Error during challenge-response:', err);
|
|
69
|
+
reject(err);
|
|
70
|
+
}
|
|
42
71
|
});
|
|
43
|
-
this.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
72
|
+
this.ws.on("message", (data) => {
|
|
73
|
+
try {
|
|
74
|
+
const message = JSON.parse(data);
|
|
75
|
+
this.handleMessage(message, resolve);
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
console.error('Error handling message:', err);
|
|
79
|
+
reject(err);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
this.ws.on("close", () => {
|
|
83
|
+
console.log('WebSocket connection closed');
|
|
84
|
+
if (this.tunnelUrl) {
|
|
85
|
+
this.emit("disconnected");
|
|
86
|
+
this.attemptReconnect();
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
// If we haven't received a tunnelUrl yet, just reject
|
|
90
|
+
reject(new Error("Connection closed before tunnel was established"));
|
|
91
|
+
}
|
|
92
|
+
});
|
|
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
|
|
99
|
+
setTimeout(() => {
|
|
100
|
+
if (!this.ws || this.ws.readyState !== ws_1.default.OPEN) {
|
|
101
|
+
const timeoutError = new Error("Connection timeout");
|
|
102
|
+
reject(timeoutError);
|
|
103
|
+
}
|
|
104
|
+
}, 10000);
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
console.error('Error creating WebSocket:', err);
|
|
108
|
+
reject(err);
|
|
109
|
+
}
|
|
64
110
|
});
|
|
65
111
|
}
|
|
66
112
|
async requestChallenge() {
|
|
@@ -99,6 +145,18 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
99
145
|
case "request":
|
|
100
146
|
this.handleRequest(message);
|
|
101
147
|
break;
|
|
148
|
+
case "websocket_connection":
|
|
149
|
+
this.handleWebSocketConnection(message);
|
|
150
|
+
break;
|
|
151
|
+
case "sse_connection":
|
|
152
|
+
this.handleSSEConnection(message);
|
|
153
|
+
break;
|
|
154
|
+
case "sse_close":
|
|
155
|
+
this.handleSSEClose(message);
|
|
156
|
+
break;
|
|
157
|
+
case "websocket":
|
|
158
|
+
this.handleWebSocketMessage(message);
|
|
159
|
+
break;
|
|
102
160
|
}
|
|
103
161
|
}
|
|
104
162
|
async handleRequest(request) {
|
|
@@ -111,6 +169,191 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
111
169
|
this.emit("request_error", { request, error });
|
|
112
170
|
}
|
|
113
171
|
}
|
|
172
|
+
handleWebSocketConnection(message) {
|
|
173
|
+
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
|
+
const client = new ws_1.default(`ws://localhost:${this.port}${message.path}`, {
|
|
177
|
+
headers: message.headers
|
|
178
|
+
});
|
|
179
|
+
// Store the client
|
|
180
|
+
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
|
+
client.on('message', (data) => {
|
|
187
|
+
if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
|
|
188
|
+
this.ws.send(JSON.stringify({
|
|
189
|
+
type: 'websocket',
|
|
190
|
+
id: message.id,
|
|
191
|
+
event: 'message',
|
|
192
|
+
data: data.toString('base64')
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
// Handle close
|
|
197
|
+
client.on('close', () => {
|
|
198
|
+
console.log(`Local WebSocket connection closed: ${message.id}`);
|
|
199
|
+
if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
|
|
200
|
+
this.ws.send(JSON.stringify({
|
|
201
|
+
type: 'websocket',
|
|
202
|
+
id: message.id,
|
|
203
|
+
event: 'close'
|
|
204
|
+
}));
|
|
205
|
+
}
|
|
206
|
+
this.webSocketClients.delete(message.id);
|
|
207
|
+
});
|
|
208
|
+
// Handle errors
|
|
209
|
+
client.on('error', (error) => {
|
|
210
|
+
console.error(`Local WebSocket connection error: ${message.id}`, error.message);
|
|
211
|
+
if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
|
|
212
|
+
this.ws.send(JSON.stringify({
|
|
213
|
+
type: 'websocket',
|
|
214
|
+
id: message.id,
|
|
215
|
+
event: 'error',
|
|
216
|
+
data: error.message
|
|
217
|
+
}));
|
|
218
|
+
}
|
|
219
|
+
this.webSocketClients.delete(message.id);
|
|
220
|
+
});
|
|
221
|
+
this.emit("websocket_connection", { id: message.id, path: message.path });
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
console.error('Error establishing WebSocket connection:', error);
|
|
225
|
+
if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
|
|
226
|
+
this.ws.send(JSON.stringify({
|
|
227
|
+
type: 'websocket',
|
|
228
|
+
id: message.id,
|
|
229
|
+
event: 'error',
|
|
230
|
+
data: error.message
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
handleWebSocketMessage(message) {
|
|
236
|
+
const client = this.webSocketClients.get(message.id);
|
|
237
|
+
if (!client)
|
|
238
|
+
return;
|
|
239
|
+
if (message.event === 'message' && message.data) {
|
|
240
|
+
const data = Buffer.from(message.data, 'base64');
|
|
241
|
+
if (client.readyState === ws_1.default.OPEN) {
|
|
242
|
+
client.send(data);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
else if (message.event === 'close') {
|
|
246
|
+
if (client.readyState === ws_1.default.OPEN) {
|
|
247
|
+
client.close();
|
|
248
|
+
}
|
|
249
|
+
this.webSocketClients.delete(message.id);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
handleSSEConnection(message) {
|
|
253
|
+
try {
|
|
254
|
+
// Create an EventSource-like stream to the local server
|
|
255
|
+
// Since Node.js doesn't have a built-in EventSource, we'll use HTTP
|
|
256
|
+
const options = {
|
|
257
|
+
hostname: 'localhost',
|
|
258
|
+
port: this.port,
|
|
259
|
+
path: message.path,
|
|
260
|
+
method: 'GET',
|
|
261
|
+
headers: message.headers,
|
|
262
|
+
};
|
|
263
|
+
const req = http_1.default.request(options, (res) => {
|
|
264
|
+
if (res.statusCode !== 200) {
|
|
265
|
+
if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
|
|
266
|
+
this.ws.send(JSON.stringify({
|
|
267
|
+
type: 'sse',
|
|
268
|
+
id: message.id,
|
|
269
|
+
event: 'error',
|
|
270
|
+
data: `Server responded with status code ${res.statusCode}`
|
|
271
|
+
}));
|
|
272
|
+
}
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
// Process SSE stream
|
|
276
|
+
let buffer = '';
|
|
277
|
+
res.on('data', (chunk) => {
|
|
278
|
+
buffer += chunk.toString();
|
|
279
|
+
// Process complete SSE messages
|
|
280
|
+
while (buffer.includes('\n\n')) {
|
|
281
|
+
const messageEndIndex = buffer.indexOf('\n\n');
|
|
282
|
+
const messageData = buffer.substring(0, messageEndIndex);
|
|
283
|
+
buffer = buffer.substring(messageEndIndex + 2);
|
|
284
|
+
// Parse the SSE message
|
|
285
|
+
const lines = messageData.split('\n');
|
|
286
|
+
let event = 'message';
|
|
287
|
+
let data = '';
|
|
288
|
+
for (const line of lines) {
|
|
289
|
+
if (line.startsWith('event:')) {
|
|
290
|
+
event = line.substring(6).trim();
|
|
291
|
+
}
|
|
292
|
+
else if (line.startsWith('data:')) {
|
|
293
|
+
data += line.substring(5).trim() + '\n';
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// Remove trailing newline
|
|
297
|
+
if (data.endsWith('\n')) {
|
|
298
|
+
data = data.substring(0, data.length - 1);
|
|
299
|
+
}
|
|
300
|
+
// Forward to server
|
|
301
|
+
if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
|
|
302
|
+
this.ws.send(JSON.stringify({
|
|
303
|
+
type: 'sse',
|
|
304
|
+
id: message.id,
|
|
305
|
+
event,
|
|
306
|
+
data
|
|
307
|
+
}));
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
res.on('end', () => {
|
|
312
|
+
if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
|
|
313
|
+
this.ws.send(JSON.stringify({
|
|
314
|
+
type: 'sse',
|
|
315
|
+
id: message.id,
|
|
316
|
+
event: 'close',
|
|
317
|
+
data: 'Connection closed'
|
|
318
|
+
}));
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
req.on('error', (error) => {
|
|
323
|
+
if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
|
|
324
|
+
this.ws.send(JSON.stringify({
|
|
325
|
+
type: 'sse',
|
|
326
|
+
id: message.id,
|
|
327
|
+
event: 'error',
|
|
328
|
+
data: error.message
|
|
329
|
+
}));
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
req.end();
|
|
333
|
+
// Store a reference to abort the connection later if needed
|
|
334
|
+
this.sseClients.set(message.id, req);
|
|
335
|
+
this.emit("sse_connection", { id: message.id, path: message.path });
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
console.error('Error establishing SSE connection:', error);
|
|
339
|
+
if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
|
|
340
|
+
this.ws.send(JSON.stringify({
|
|
341
|
+
type: 'sse',
|
|
342
|
+
id: message.id,
|
|
343
|
+
event: 'error',
|
|
344
|
+
data: error.message
|
|
345
|
+
}));
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
handleSSEClose(message) {
|
|
350
|
+
const client = this.sseClients.get(message.id);
|
|
351
|
+
if (client) {
|
|
352
|
+
// Abort the request if it's still active
|
|
353
|
+
client.destroy();
|
|
354
|
+
this.sseClients.delete(message.id);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
114
357
|
forwardRequest(request) {
|
|
115
358
|
return new Promise((resolve, reject) => {
|
|
116
359
|
const options = {
|
|
@@ -153,19 +396,73 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
153
396
|
attemptReconnect() {
|
|
154
397
|
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
155
398
|
this.reconnectAttempts++;
|
|
156
|
-
|
|
399
|
+
console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
|
|
400
|
+
setTimeout(async () => {
|
|
401
|
+
try {
|
|
402
|
+
await this.connect();
|
|
403
|
+
// If the connect succeeds but we don't have a tunnelUrl yet, we're still initializing
|
|
404
|
+
if (!this.tunnelUrl) {
|
|
405
|
+
console.log('Reconnected but tunnelUrl not set. Waiting for tunnel URL...');
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
console.log(`Reconnected successfully. Tunnel URL: ${this.tunnelUrl}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
console.error(`Reconnection attempt ${this.reconnectAttempts} failed:`, error);
|
|
413
|
+
}
|
|
414
|
+
}, this.reconnectDelay);
|
|
157
415
|
}
|
|
158
416
|
else {
|
|
417
|
+
console.log('Maximum reconnection attempts reached');
|
|
159
418
|
this.emit("max_reconnect_attempts");
|
|
160
419
|
}
|
|
161
420
|
}
|
|
162
421
|
async stop() {
|
|
163
|
-
return new Promise((resolve) => {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
this.
|
|
422
|
+
return new Promise(async (resolve) => {
|
|
423
|
+
try {
|
|
424
|
+
// Close all WebSocket clients
|
|
425
|
+
for (const [id, client] of this.webSocketClients.entries()) {
|
|
426
|
+
try {
|
|
427
|
+
if (client.readyState === ws_1.default.OPEN) {
|
|
428
|
+
client.close();
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
432
|
+
console.error(`Error closing WebSocket client ${id}:`, error);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
this.webSocketClients.clear();
|
|
436
|
+
// Close all SSE clients
|
|
437
|
+
for (const [id, client] of this.sseClients.entries()) {
|
|
438
|
+
try {
|
|
439
|
+
client.destroy();
|
|
440
|
+
}
|
|
441
|
+
catch (error) {
|
|
442
|
+
console.error(`Error destroying SSE client ${id}:`, error);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
this.sseClients.clear();
|
|
446
|
+
// Close main WebSocket connection
|
|
447
|
+
if (this.ws) {
|
|
448
|
+
try {
|
|
449
|
+
if (this.ws.readyState === ws_1.default.OPEN) {
|
|
450
|
+
this.ws.close();
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
catch (error) {
|
|
454
|
+
console.error('Error closing main WebSocket:', error);
|
|
455
|
+
}
|
|
456
|
+
this.ws = null;
|
|
457
|
+
}
|
|
458
|
+
// Wait for all connections to close properly
|
|
459
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
460
|
+
resolve();
|
|
461
|
+
}
|
|
462
|
+
catch (error) {
|
|
463
|
+
console.error('Error in stop method:', error);
|
|
464
|
+
resolve(); // Resolve anyway to not block cleaning up
|
|
167
465
|
}
|
|
168
|
-
resolve();
|
|
169
466
|
});
|
|
170
467
|
}
|
|
171
468
|
}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -7,13 +7,20 @@ declare class DainTunnelServer {
|
|
|
7
7
|
private tunnels;
|
|
8
8
|
private pendingRequests;
|
|
9
9
|
private challenges;
|
|
10
|
+
private sseConnections;
|
|
11
|
+
private wsConnections;
|
|
10
12
|
constructor(hostname: string, port: number);
|
|
11
13
|
private setupExpressRoutes;
|
|
12
14
|
private setupWebSocketServer;
|
|
15
|
+
private handleTunnelClientConnection;
|
|
16
|
+
private handleProxiedWebSocketConnection;
|
|
13
17
|
private handleChallengeRequest;
|
|
14
18
|
private handleStartMessage;
|
|
15
19
|
private handleResponseMessage;
|
|
16
|
-
private
|
|
20
|
+
private handleSSEMessage;
|
|
21
|
+
private handleWebSocketMessage;
|
|
22
|
+
private handleRequest;
|
|
23
|
+
private handleSSERequest;
|
|
17
24
|
private removeTunnel;
|
|
18
25
|
start(): Promise<void>;
|
|
19
26
|
stop(): Promise<void>;
|
package/dist/server/index.js
CHANGED
|
@@ -9,8 +9,9 @@ const ws_1 = __importDefault(require("ws"));
|
|
|
9
9
|
const uuid_1 = require("uuid");
|
|
10
10
|
const body_parser_1 = __importDefault(require("body-parser"));
|
|
11
11
|
const cors_1 = __importDefault(require("cors"));
|
|
12
|
-
const
|
|
13
|
-
const
|
|
12
|
+
const auth_1 = require("@dainprotocol/service-sdk/service/auth");
|
|
13
|
+
const crypto_1 = require("crypto");
|
|
14
|
+
const url_1 = require("url");
|
|
14
15
|
class DainTunnelServer {
|
|
15
16
|
constructor(hostname, port) {
|
|
16
17
|
this.hostname = hostname;
|
|
@@ -18,11 +19,20 @@ class DainTunnelServer {
|
|
|
18
19
|
this.tunnels = new Map();
|
|
19
20
|
this.pendingRequests = new Map();
|
|
20
21
|
this.challenges = new Map();
|
|
22
|
+
this.sseConnections = new Map();
|
|
23
|
+
this.wsConnections = new Map();
|
|
21
24
|
this.app = (0, express_1.default)();
|
|
22
25
|
this.server = http_1.default.createServer(this.app);
|
|
23
|
-
this.wss = new ws_1.default.Server({
|
|
26
|
+
this.wss = new ws_1.default.Server({
|
|
27
|
+
server: this.server,
|
|
28
|
+
path: undefined // Allow connections on any path
|
|
29
|
+
});
|
|
24
30
|
// Use cors middleware
|
|
25
|
-
this.app.use((0, cors_1.default)(
|
|
31
|
+
this.app.use((0, cors_1.default)({
|
|
32
|
+
origin: '*',
|
|
33
|
+
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
|
34
|
+
credentials: true
|
|
35
|
+
}));
|
|
26
36
|
// Update CORS middleware
|
|
27
37
|
this.app.use((req, res, next) => {
|
|
28
38
|
res.header("Access-Control-Allow-Origin", "*");
|
|
@@ -39,37 +49,132 @@ class DainTunnelServer {
|
|
|
39
49
|
this.setupWebSocketServer();
|
|
40
50
|
}
|
|
41
51
|
setupExpressRoutes() {
|
|
42
|
-
|
|
52
|
+
// Generic route handler for all tunnel requests
|
|
53
|
+
this.app.use("/:tunnelId", this.handleRequest.bind(this));
|
|
43
54
|
}
|
|
44
55
|
setupWebSocketServer() {
|
|
45
|
-
|
|
56
|
+
// Handle WebSocket connections from tunnel clients
|
|
57
|
+
this.wss.on("connection", (ws, req) => {
|
|
58
|
+
var _a;
|
|
46
59
|
console.log("New WebSocket connection");
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
else if (data.type === "response") {
|
|
58
|
-
this.handleResponseMessage(data);
|
|
59
|
-
}
|
|
60
|
+
try {
|
|
61
|
+
// Check if this is a tunnel client connection or a user connection to be proxied
|
|
62
|
+
const url = (0, url_1.parse)(req.url || '', true);
|
|
63
|
+
const pathParts = ((_a = url.pathname) === null || _a === void 0 ? void 0 : _a.split('/')) || [];
|
|
64
|
+
console.log(`WebSocket connection path: ${url.pathname}`);
|
|
65
|
+
// Client tunnel connection has no tunnelId in the path (or is at root)
|
|
66
|
+
if (!url.pathname || url.pathname === '/' || pathParts.length <= 1 || !pathParts[1]) {
|
|
67
|
+
console.log("Handling as tunnel client connection");
|
|
68
|
+
this.handleTunnelClientConnection(ws, req);
|
|
60
69
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
70
|
+
else {
|
|
71
|
+
// This is a WebSocket connection to be proxied through the tunnel
|
|
72
|
+
console.log(`Handling as proxied WebSocket connection for path: ${url.pathname}`);
|
|
73
|
+
this.handleProxiedWebSocketConnection(ws, req);
|
|
64
74
|
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
console.
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
console.error("Error in WebSocket connection setup:", error);
|
|
78
|
+
ws.close(1011, "Internal server error");
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
// Handle WebSocket connections from tunnel clients
|
|
83
|
+
handleTunnelClientConnection(ws, req) {
|
|
84
|
+
ws.on("message", (message) => {
|
|
85
|
+
try {
|
|
86
|
+
const data = JSON.parse(message);
|
|
87
|
+
console.log(`Received WebSocket message: ${data.type}`);
|
|
88
|
+
if (data.type === "challenge_request") {
|
|
89
|
+
this.handleChallengeRequest(ws);
|
|
90
|
+
}
|
|
91
|
+
else if (data.type === "start") {
|
|
92
|
+
this.handleStartMessage(ws, data);
|
|
93
|
+
}
|
|
94
|
+
else if (data.type === "response") {
|
|
95
|
+
this.handleResponseMessage(data);
|
|
96
|
+
}
|
|
97
|
+
else if (data.type === "sse") {
|
|
98
|
+
this.handleSSEMessage(data);
|
|
99
|
+
}
|
|
100
|
+
else if (data.type === "websocket") {
|
|
101
|
+
this.handleWebSocketMessage(data);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
console.error("Error processing message:", error);
|
|
106
|
+
ws.close(1008, "Invalid message");
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
ws.on("close", () => {
|
|
110
|
+
console.log("WebSocket connection closed");
|
|
111
|
+
this.removeTunnel(ws);
|
|
112
|
+
});
|
|
113
|
+
ws.on("error", (error) => {
|
|
114
|
+
console.error("WebSocket error:", error);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
// Handle incoming WebSocket connections to be proxied through the tunnel
|
|
118
|
+
handleProxiedWebSocketConnection(ws, req) {
|
|
119
|
+
var _a;
|
|
120
|
+
const url = (0, url_1.parse)(req.url || '', true);
|
|
121
|
+
const pathParts = ((_a = url.pathname) === null || _a === void 0 ? void 0 : _a.split('/')) || [];
|
|
122
|
+
if (pathParts.length < 2) {
|
|
123
|
+
ws.close(1008, "Invalid tunnel ID");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const tunnelId = pathParts[1];
|
|
127
|
+
const remainingPath = '/' + pathParts.slice(2).join('/');
|
|
128
|
+
const tunnel = this.tunnels.get(tunnelId);
|
|
129
|
+
console.log(`Handling WebSocket connection for tunnel: ${tunnelId}, path: ${remainingPath}`);
|
|
130
|
+
console.log(`Available tunnels: ${Array.from(this.tunnels.keys()).join(', ')}`);
|
|
131
|
+
if (!tunnel) {
|
|
132
|
+
console.log(`Tunnel not found for WebSocket connection: ${tunnelId}`);
|
|
133
|
+
ws.close(1008, "Tunnel not found");
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
// Create a WebSocket connection ID
|
|
137
|
+
const wsConnectionId = (0, uuid_1.v4)();
|
|
138
|
+
// Store this connection
|
|
139
|
+
this.wsConnections.set(wsConnectionId, {
|
|
140
|
+
clientSocket: ws,
|
|
141
|
+
id: wsConnectionId,
|
|
142
|
+
path: remainingPath,
|
|
143
|
+
headers: req.headers
|
|
144
|
+
});
|
|
145
|
+
// Notify tunnel client about the new WebSocket connection
|
|
146
|
+
tunnel.ws.send(JSON.stringify({
|
|
147
|
+
type: "websocket_connection",
|
|
148
|
+
id: wsConnectionId,
|
|
149
|
+
path: remainingPath,
|
|
150
|
+
headers: req.headers
|
|
151
|
+
}));
|
|
152
|
+
// Handle messages from the client
|
|
153
|
+
ws.on("message", (data) => {
|
|
154
|
+
tunnel.ws.send(JSON.stringify({
|
|
155
|
+
type: "websocket",
|
|
156
|
+
id: wsConnectionId,
|
|
157
|
+
event: "message",
|
|
158
|
+
data: data.toString('base64')
|
|
159
|
+
}));
|
|
160
|
+
});
|
|
161
|
+
// Handle client disconnection
|
|
162
|
+
ws.on("close", () => {
|
|
163
|
+
tunnel.ws.send(JSON.stringify({
|
|
164
|
+
type: "websocket",
|
|
165
|
+
id: wsConnectionId,
|
|
166
|
+
event: "close"
|
|
167
|
+
}));
|
|
168
|
+
this.wsConnections.delete(wsConnectionId);
|
|
169
|
+
});
|
|
170
|
+
// Handle errors
|
|
171
|
+
ws.on("error", (error) => {
|
|
172
|
+
tunnel.ws.send(JSON.stringify({
|
|
173
|
+
type: "websocket",
|
|
174
|
+
id: wsConnectionId,
|
|
175
|
+
event: "error",
|
|
176
|
+
data: error.message
|
|
177
|
+
}));
|
|
73
178
|
});
|
|
74
179
|
}
|
|
75
180
|
handleChallengeRequest(ws) {
|
|
@@ -79,41 +184,96 @@ class DainTunnelServer {
|
|
|
79
184
|
ws.send(JSON.stringify({ type: "challenge", challenge }));
|
|
80
185
|
}
|
|
81
186
|
handleStartMessage(ws, data) {
|
|
82
|
-
const { challenge, signature, tunnelId } = data;
|
|
187
|
+
const { challenge, signature, tunnelId, apiKey } = data;
|
|
83
188
|
const challengeObj = this.challenges.get(challenge);
|
|
84
189
|
if (!challengeObj || challengeObj.ws !== ws) {
|
|
85
190
|
ws.close(1008, "Invalid challenge");
|
|
86
191
|
return;
|
|
87
192
|
}
|
|
88
193
|
this.challenges.delete(challenge);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
194
|
+
try {
|
|
195
|
+
// Parse API key to get the secret for HMAC validation
|
|
196
|
+
if (!apiKey) {
|
|
197
|
+
ws.close(1008, "API key required");
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const parsed = (0, auth_1.parseAPIKey)(apiKey);
|
|
201
|
+
if (!parsed) {
|
|
202
|
+
ws.close(1008, "Invalid API key format");
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
// Validate HMAC signature
|
|
206
|
+
const expectedSignature = (0, crypto_1.createHmac)('sha256', parsed.secret)
|
|
207
|
+
.update(challenge)
|
|
208
|
+
.digest('hex');
|
|
209
|
+
if (expectedSignature !== signature) {
|
|
210
|
+
ws.close(1008, "Invalid signature");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
// Verify that tunnelId matches the agentId from the API key
|
|
214
|
+
if (tunnelId !== parsed.agentId) {
|
|
215
|
+
ws.close(1008, "Tunnel ID does not match API key");
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
// If tunnel already exists, remove old one
|
|
99
219
|
if (this.tunnels.has(tunnelId)) {
|
|
100
|
-
console.log(`Tunnel ${tunnelId}
|
|
220
|
+
console.log(`Tunnel ${tunnelId} already exists, replacing it`);
|
|
221
|
+
const oldTunnel = this.tunnels.get(tunnelId);
|
|
222
|
+
if (oldTunnel && oldTunnel.ws !== ws) {
|
|
223
|
+
oldTunnel.ws.close(1000, "Replaced by new connection");
|
|
224
|
+
}
|
|
101
225
|
}
|
|
102
|
-
|
|
103
|
-
|
|
226
|
+
this.tunnels.set(tunnelId, { id: tunnelId, ws });
|
|
227
|
+
console.log(`Tunnel added: ${tunnelId}`);
|
|
228
|
+
console.log(`Current tunnels: ${Array.from(this.tunnels.keys()).join(', ')}`);
|
|
229
|
+
// Save tunnel ID on the WebSocket object for easy lookup on close
|
|
230
|
+
ws.tunnelId = tunnelId;
|
|
231
|
+
// Add a periodic check to ensure the tunnel is still in the map
|
|
232
|
+
const intervalId = setInterval(() => {
|
|
233
|
+
if (this.tunnels.has(tunnelId)) {
|
|
234
|
+
const tunnel = this.tunnels.get(tunnelId);
|
|
235
|
+
if (tunnel && tunnel.ws === ws && ws.readyState === ws_1.default.OPEN) {
|
|
236
|
+
console.log(`Tunnel ${tunnelId} still active`);
|
|
237
|
+
// Send a ping to keep the connection alive
|
|
238
|
+
try {
|
|
239
|
+
ws.ping();
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
console.error(`Error sending ping to tunnel ${tunnelId}:`, error);
|
|
243
|
+
clearInterval(intervalId);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
console.log(`Tunnel ${tunnelId} exists but WebSocket state is invalid, cleaning up`);
|
|
248
|
+
clearInterval(intervalId);
|
|
249
|
+
if (tunnel && tunnel.ws === ws) {
|
|
250
|
+
this.tunnels.delete(tunnelId);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
console.log(`Tunnel ${tunnelId} not found in periodic check`);
|
|
256
|
+
clearInterval(intervalId);
|
|
257
|
+
}
|
|
258
|
+
}, 5000); // Check every 5 seconds
|
|
259
|
+
// Store the interval ID on the WebSocket object so we can clean it up
|
|
260
|
+
ws.keepAliveInterval = intervalId;
|
|
261
|
+
ws.on("close", () => {
|
|
262
|
+
console.log(`WebSocket for tunnel ${tunnelId} closed, clearing interval`);
|
|
104
263
|
clearInterval(intervalId);
|
|
264
|
+
});
|
|
265
|
+
let tunnelUrl = `${this.hostname}`;
|
|
266
|
+
if (process.env.SKIP_PORT !== "true") {
|
|
267
|
+
tunnelUrl += `:${this.port}`;
|
|
105
268
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
269
|
+
tunnelUrl += `/${tunnelId}`;
|
|
270
|
+
ws.send(JSON.stringify({ type: "tunnelUrl", url: tunnelUrl }));
|
|
271
|
+
console.log(`New tunnel created: ${tunnelUrl}`);
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
console.error(`Error in handleStartMessage for tunnel ${tunnelId}:`, error);
|
|
275
|
+
ws.close(1011, "Internal server error");
|
|
113
276
|
}
|
|
114
|
-
tunnelUrl += `/${tunnelId}`;
|
|
115
|
-
ws.send(JSON.stringify({ type: "tunnelUrl", url: tunnelUrl }));
|
|
116
|
-
console.log(`New tunnel created: ${tunnelUrl}`);
|
|
117
277
|
}
|
|
118
278
|
handleResponseMessage(data) {
|
|
119
279
|
const pendingRequest = this.pendingRequests.get(data.requestId);
|
|
@@ -133,8 +293,61 @@ class DainTunnelServer {
|
|
|
133
293
|
this.pendingRequests.delete(data.requestId);
|
|
134
294
|
}
|
|
135
295
|
}
|
|
136
|
-
|
|
296
|
+
handleSSEMessage(data) {
|
|
297
|
+
const connection = this.sseConnections.get(data.id);
|
|
298
|
+
if (!connection) {
|
|
299
|
+
console.log(`SSE connection not found: ${data.id}`);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const { res } = connection;
|
|
303
|
+
if (data.event) {
|
|
304
|
+
res.write(`event: ${data.event}\n`);
|
|
305
|
+
}
|
|
306
|
+
// Split data by newlines and send each line
|
|
307
|
+
const dataLines = data.data.split('\n');
|
|
308
|
+
for (const line of dataLines) {
|
|
309
|
+
res.write(`data: ${line}\n`);
|
|
310
|
+
}
|
|
311
|
+
res.write('\n');
|
|
312
|
+
// Check if this is a "close" event
|
|
313
|
+
if (data.event === 'close') {
|
|
314
|
+
res.end();
|
|
315
|
+
this.sseConnections.delete(data.id);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
handleWebSocketMessage(data) {
|
|
319
|
+
const connection = this.wsConnections.get(data.id);
|
|
320
|
+
if (!connection) {
|
|
321
|
+
console.log(`WebSocket connection not found: ${data.id}`);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const { clientSocket } = connection;
|
|
325
|
+
if (data.event === 'message' && data.data) {
|
|
326
|
+
const messageData = Buffer.from(data.data, 'base64');
|
|
327
|
+
if (clientSocket.readyState === ws_1.default.OPEN) {
|
|
328
|
+
clientSocket.send(messageData);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
else if (data.event === 'close') {
|
|
332
|
+
if (clientSocket.readyState === ws_1.default.OPEN) {
|
|
333
|
+
clientSocket.close();
|
|
334
|
+
}
|
|
335
|
+
this.wsConnections.delete(data.id);
|
|
336
|
+
}
|
|
337
|
+
else if (data.event === 'error' && data.data) {
|
|
338
|
+
if (clientSocket.readyState === ws_1.default.OPEN) {
|
|
339
|
+
clientSocket.close(1011, data.data);
|
|
340
|
+
}
|
|
341
|
+
this.wsConnections.delete(data.id);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
async handleRequest(req, res) {
|
|
137
345
|
const tunnelId = req.params.tunnelId;
|
|
346
|
+
// Check for upgraded connections (WebSockets) - these are handled by the WebSocket server
|
|
347
|
+
if (req.headers.upgrade && req.headers.upgrade.toLowerCase() === 'websocket') {
|
|
348
|
+
// This is handled by the WebSocket server now
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
138
351
|
let tunnel;
|
|
139
352
|
let retries = 3;
|
|
140
353
|
while (retries > 0 && !tunnel) {
|
|
@@ -150,6 +363,11 @@ class DainTunnelServer {
|
|
|
150
363
|
console.log(`Tunnel not found after retries: ${tunnelId}`);
|
|
151
364
|
return res.status(404).send("Tunnel not found");
|
|
152
365
|
}
|
|
366
|
+
// Check for SSE request
|
|
367
|
+
if (req.headers.accept && req.headers.accept.includes('text/event-stream')) {
|
|
368
|
+
return this.handleSSERequest(req, res, tunnelId, tunnel);
|
|
369
|
+
}
|
|
370
|
+
// Handle regular HTTP request
|
|
153
371
|
const requestId = (0, uuid_1.v4)();
|
|
154
372
|
const startTime = Date.now();
|
|
155
373
|
this.pendingRequests.set(requestId, { res, startTime });
|
|
@@ -166,28 +384,109 @@ class DainTunnelServer {
|
|
|
166
384
|
tunnel.ws.send(JSON.stringify(requestMessage));
|
|
167
385
|
console.log(`Request forwarded: ${requestId}, Method: ${req.method}, Path: ${req.url}`);
|
|
168
386
|
}
|
|
387
|
+
handleSSERequest(req, res, tunnelId, tunnel) {
|
|
388
|
+
// Setup SSE connection
|
|
389
|
+
const sseId = (0, uuid_1.v4)();
|
|
390
|
+
res.writeHead(200, {
|
|
391
|
+
'Content-Type': 'text/event-stream',
|
|
392
|
+
'Cache-Control': 'no-cache',
|
|
393
|
+
'Connection': 'keep-alive'
|
|
394
|
+
});
|
|
395
|
+
// Send initial connection event
|
|
396
|
+
res.write('\n');
|
|
397
|
+
// Store the SSE connection
|
|
398
|
+
this.sseConnections.set(sseId, {
|
|
399
|
+
req,
|
|
400
|
+
res,
|
|
401
|
+
id: sseId,
|
|
402
|
+
tunnelId
|
|
403
|
+
});
|
|
404
|
+
// Notify the tunnel client about the new SSE connection
|
|
405
|
+
tunnel.ws.send(JSON.stringify({
|
|
406
|
+
type: "sse_connection",
|
|
407
|
+
id: sseId,
|
|
408
|
+
path: req.url,
|
|
409
|
+
headers: req.headers
|
|
410
|
+
}));
|
|
411
|
+
// Handle client disconnect
|
|
412
|
+
req.on('close', () => {
|
|
413
|
+
tunnel.ws.send(JSON.stringify({
|
|
414
|
+
type: "sse_close",
|
|
415
|
+
id: sseId
|
|
416
|
+
}));
|
|
417
|
+
this.sseConnections.delete(sseId);
|
|
418
|
+
});
|
|
419
|
+
}
|
|
169
420
|
removeTunnel(ws) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
removedTunnelId = id;
|
|
175
|
-
console.log(`Tunnel removed: ${id}`);
|
|
176
|
-
break;
|
|
421
|
+
try {
|
|
422
|
+
// Clear any interval timer
|
|
423
|
+
if (ws.keepAliveInterval) {
|
|
424
|
+
clearInterval(ws.keepAliveInterval);
|
|
177
425
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
426
|
+
// If we saved tunnelId on the WebSocket object, use it for faster lookup
|
|
427
|
+
const tunnelId = ws.tunnelId;
|
|
428
|
+
let removedTunnelId = tunnelId;
|
|
429
|
+
if (tunnelId && this.tunnels.has(tunnelId)) {
|
|
430
|
+
const tunnel = this.tunnels.get(tunnelId);
|
|
431
|
+
if (tunnel && tunnel.ws === ws) {
|
|
432
|
+
this.tunnels.delete(tunnelId);
|
|
433
|
+
console.log(`Tunnel removed using stored ID: ${tunnelId}`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
// Fall back to iterating through all tunnels
|
|
438
|
+
removedTunnelId = undefined;
|
|
439
|
+
for (const [id, tunnel] of this.tunnels.entries()) {
|
|
440
|
+
if (tunnel.ws === ws) {
|
|
441
|
+
this.tunnels.delete(id);
|
|
442
|
+
removedTunnelId = id;
|
|
443
|
+
console.log(`Tunnel removed: ${id}`);
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (removedTunnelId) {
|
|
449
|
+
console.log(`Tunnel ${removedTunnelId} removed. Current tunnels: ${Array.from(this.tunnels.keys()).join(', ')}`);
|
|
450
|
+
// Close all SSE connections associated with this tunnel
|
|
451
|
+
for (const [sseId, sseConnection] of this.sseConnections.entries()) {
|
|
452
|
+
if (sseConnection.tunnelId === removedTunnelId) {
|
|
453
|
+
try {
|
|
454
|
+
sseConnection.res.end();
|
|
455
|
+
}
|
|
456
|
+
catch (error) {
|
|
457
|
+
console.error(`Error closing SSE connection ${sseId}:`, error);
|
|
458
|
+
}
|
|
459
|
+
this.sseConnections.delete(sseId);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
// Close all WebSocket connections associated with this tunnel
|
|
463
|
+
for (const [wsId, wsConnection] of this.wsConnections.entries()) {
|
|
464
|
+
const wsPath = wsConnection.path;
|
|
465
|
+
if (wsPath.startsWith(`/${removedTunnelId}/`)) {
|
|
466
|
+
try {
|
|
467
|
+
wsConnection.clientSocket.close(1001, "Tunnel closed");
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
console.error(`Error closing WebSocket connection ${wsId}:`, error);
|
|
471
|
+
}
|
|
472
|
+
this.wsConnections.delete(wsId);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
console.log(`No tunnel found to remove for the closed WebSocket connection`);
|
|
190
478
|
}
|
|
479
|
+
// Also remove any pending challenges for this WebSocket
|
|
480
|
+
for (const [challenge, challengeObj] of this.challenges.entries()) {
|
|
481
|
+
if (challengeObj.ws === ws) {
|
|
482
|
+
this.challenges.delete(challenge);
|
|
483
|
+
console.log(`Challenge removed for closed WebSocket`);
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
catch (error) {
|
|
489
|
+
console.error("Error in removeTunnel:", error);
|
|
191
490
|
}
|
|
192
491
|
}
|
|
193
492
|
async start() {
|
|
@@ -200,11 +499,39 @@ class DainTunnelServer {
|
|
|
200
499
|
}
|
|
201
500
|
async stop() {
|
|
202
501
|
return new Promise((resolve) => {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
502
|
+
try {
|
|
503
|
+
// Close all SSE connections
|
|
504
|
+
for (const [sseId, sseConnection] of this.sseConnections.entries()) {
|
|
505
|
+
try {
|
|
506
|
+
sseConnection.res.end();
|
|
507
|
+
}
|
|
508
|
+
catch (error) {
|
|
509
|
+
console.error(`Error closing SSE connection ${sseId}:`, error);
|
|
510
|
+
}
|
|
511
|
+
this.sseConnections.delete(sseId);
|
|
512
|
+
}
|
|
513
|
+
// Close all WebSocket connections
|
|
514
|
+
for (const [wsId, wsConnection] of this.wsConnections.entries()) {
|
|
515
|
+
try {
|
|
516
|
+
wsConnection.clientSocket.close(1001, "Server shutting down");
|
|
517
|
+
}
|
|
518
|
+
catch (error) {
|
|
519
|
+
console.error(`Error closing WebSocket connection ${wsId}:`, error);
|
|
520
|
+
}
|
|
521
|
+
this.wsConnections.delete(wsId);
|
|
522
|
+
}
|
|
523
|
+
// Close the WebSocket server
|
|
524
|
+
this.wss.close(() => {
|
|
525
|
+
// Close the HTTP server
|
|
526
|
+
this.server.close(() => {
|
|
527
|
+
resolve();
|
|
528
|
+
});
|
|
206
529
|
});
|
|
207
|
-
}
|
|
530
|
+
}
|
|
531
|
+
catch (error) {
|
|
532
|
+
console.error('Error during server shutdown:', error);
|
|
533
|
+
resolve(); // Resolve anyway to prevent hanging
|
|
534
|
+
}
|
|
208
535
|
});
|
|
209
536
|
}
|
|
210
537
|
}
|
package/dist/server/start.js
CHANGED
|
@@ -13,6 +13,7 @@ const server = new index_1.default(hostname, port);
|
|
|
13
13
|
server.start()
|
|
14
14
|
.then(() => {
|
|
15
15
|
console.log(`DainTunnel Server started on http://${hostname}:${port}`);
|
|
16
|
+
console.log(`Supports HTTP, WebSockets, and Server-Sent Events (SSE)`);
|
|
16
17
|
})
|
|
17
18
|
.catch((error) => {
|
|
18
19
|
console.error('Failed to start DainTunnel Server:', error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dainprotocol/tunnel",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"private": false,
|
|
@@ -19,9 +19,10 @@
|
|
|
19
19
|
"author": "Ryan",
|
|
20
20
|
"license": "ISC",
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@dainprotocol/service-sdk": "^1.0
|
|
22
|
+
"@dainprotocol/service-sdk": "^1.3.0",
|
|
23
23
|
"@types/body-parser": "^1.19.5",
|
|
24
24
|
"@types/cors": "^2.8.17",
|
|
25
|
+
"@types/eventsource": "^3.0.0",
|
|
25
26
|
"@types/express": "^4.17.21",
|
|
26
27
|
"@types/node": "^22.5.4",
|
|
27
28
|
"@types/uuid": "^10.0.0",
|
|
@@ -30,6 +31,7 @@
|
|
|
30
31
|
"bs58": "^6.0.0",
|
|
31
32
|
"cors": "^2.8.5",
|
|
32
33
|
"dotenv": "^16.4.5",
|
|
34
|
+
"eventsource": "^3.0.6",
|
|
33
35
|
"express": "^4.19.2",
|
|
34
36
|
"fetch-mock": "^11.1.3",
|
|
35
37
|
"ts-node": "^10.9.2",
|
|
@@ -43,13 +45,12 @@
|
|
|
43
45
|
"ts-jest": "^29.1.5",
|
|
44
46
|
"typescript": "^5.0.0"
|
|
45
47
|
},
|
|
46
|
-
|
|
47
48
|
"files": [
|
|
48
49
|
"dist",
|
|
49
50
|
"README.md"
|
|
50
51
|
],
|
|
51
52
|
"engines": {
|
|
52
|
-
"node": "20.7.0"
|
|
53
|
+
"node": ">=20.7.0"
|
|
53
54
|
},
|
|
54
55
|
"exports": {
|
|
55
56
|
".": "./dist/index.js",
|