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