@remotelinker/reverse-ws-tunnel 1.0.7 → 1.0.9
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/README.md +14 -0
- package/client/tunnelClient.js +111 -22
- package/package.json +1 -1
- package/server/constants.js +2 -0
- package/server/messageHandler.js +21 -1
- package/server/tcpServer.js +9 -4
package/README.md
CHANGED
|
@@ -30,6 +30,20 @@ Reverse WebSocket Tunnel is a library that enables you to expose local services
|
|
|
30
30
|
|
|
31
31
|
---
|
|
32
32
|
|
|
33
|
+
## ✨ v1.0.9 - What's New
|
|
34
|
+
|
|
35
|
+
### 🐛 Bug Fixes
|
|
36
|
+
- **Message Format Standardization**: Fixed inconsistent message formats between server components
|
|
37
|
+
- **Ping/Pong Reliability**: Resolved issues with application-level heartbeat failing during data transfer
|
|
38
|
+
- **Connection Stability**: Improved connection handling and reduced timeout issues
|
|
39
|
+
|
|
40
|
+
### 🔧 Technical Improvements
|
|
41
|
+
- **Unified Message Protocol**: All server messages now use consistent `buildMessageBuffer` format
|
|
42
|
+
- **Simplified Client Architecture**: Removed hybrid parsing logic for better maintainability
|
|
43
|
+
- **Enhanced Buffer Management**: Improved message buffering and parsing reliability
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
33
47
|
## 📦 Installation
|
|
34
48
|
|
|
35
49
|
```bash
|
package/client/tunnelClient.js
CHANGED
|
@@ -9,12 +9,17 @@ const packageJson = require('../package.json');
|
|
|
9
9
|
const RECONNECT_INTERVAL = 5000;
|
|
10
10
|
const MESSAGE_TYPE_CONFIG = 0x01;
|
|
11
11
|
const MESSAGE_TYPE_DATA = 0x02;
|
|
12
|
+
const MESSAGE_TYPE_APP_PING = 0x03;
|
|
13
|
+
const MESSAGE_TYPE_APP_PONG = 0x04;
|
|
12
14
|
const clients = {};
|
|
13
15
|
const PING_INTERVAL = 30 * 1000; //30s
|
|
14
16
|
const PONG_WAIT = 5 * 1000; //5s
|
|
17
|
+
const APP_PING_INTERVAL = 20 * 1000; // 20 secondi
|
|
18
|
+
const HEALTH_TIMEOUT = 45 * 1000; // 45 secondi sliding window
|
|
19
|
+
const RECONNECT_BACKOFF = [1000, 2000, 5000, 10000, 30000]; // Backoff progressivo
|
|
15
20
|
|
|
16
21
|
/**
|
|
17
|
-
* Starts
|
|
22
|
+
* Starts WebSocket tunnel client.
|
|
18
23
|
* @param {Object} config - Configuration for tunnel.
|
|
19
24
|
*/
|
|
20
25
|
function connectWebSocket(config) {
|
|
@@ -23,7 +28,12 @@ function connectWebSocket(config) {
|
|
|
23
28
|
const eventEmitter = new EventEmitter();
|
|
24
29
|
let ws;
|
|
25
30
|
let pingInterval;
|
|
31
|
+
let appPingInterval;
|
|
32
|
+
let healthMonitor;
|
|
26
33
|
let isClosed = false;
|
|
34
|
+
let reconnectAttempt = 0;
|
|
35
|
+
let pingSeq = 0;
|
|
36
|
+
let lastPongTs = Date.now();
|
|
27
37
|
|
|
28
38
|
if (!tunnelId) {
|
|
29
39
|
throw new Error(`Missing mandatory tunnelId`);
|
|
@@ -49,6 +59,10 @@ function connectWebSocket(config) {
|
|
|
49
59
|
eventEmitter.emit('connected');
|
|
50
60
|
({ pingInterval } = heartBeat(ws));
|
|
51
61
|
|
|
62
|
+
// Avviare heartbeat applicativo
|
|
63
|
+
appPingInterval = startAppHeartbeat(ws, tunnelId, { pingSeq: () => pingSeq, incPingSeq: () => pingSeq++ });
|
|
64
|
+
healthMonitor = startHealthMonitor(ws, tunnelId, { lastPongTs: () => lastPongTs, setLastPongTs: (ts) => lastPongTs = ts });
|
|
65
|
+
|
|
52
66
|
const uuid = uuidv4();
|
|
53
67
|
const payload = {
|
|
54
68
|
TARGET_URL: targetUrl,
|
|
@@ -64,29 +78,62 @@ function connectWebSocket(config) {
|
|
|
64
78
|
ws.send(message);
|
|
65
79
|
});
|
|
66
80
|
|
|
81
|
+
let messageBuffer = Buffer.alloc(0);
|
|
82
|
+
|
|
67
83
|
ws.on('message', (data) => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
84
|
+
logger.trace(`Received message chunk: ${data.length} bytes`);
|
|
85
|
+
messageBuffer = Buffer.concat([messageBuffer, data]);
|
|
86
|
+
|
|
87
|
+
while (messageBuffer.length >= 4) {
|
|
88
|
+
const length = messageBuffer.readUInt32BE(0);
|
|
89
|
+
if (messageBuffer.length < 4 + length) {
|
|
90
|
+
logger.trace(`Waiting for more data: need ${4 + length} bytes, have ${messageBuffer.length}`);
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const message = messageBuffer.slice(4, 4 + length);
|
|
95
|
+
messageBuffer = messageBuffer.slice(4 + length);
|
|
96
|
+
|
|
97
|
+
const messageTunnelId = message.slice(0, 36).toString();
|
|
98
|
+
const uuid = message.slice(36, 72).toString();
|
|
99
|
+
const type = message.readUInt8(72);
|
|
100
|
+
const payload = message.slice(73);
|
|
71
101
|
|
|
72
|
-
|
|
102
|
+
logger.trace(`Received WS message for uuid=${uuid}, type=${type}, length=${payload.length}`);
|
|
73
103
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
104
|
+
if (type === MESSAGE_TYPE_DATA) {
|
|
105
|
+
if (payload.toString() === 'CLOSE') {
|
|
106
|
+
logger.debug(`Received CLOSE for uuid=${uuid}`);
|
|
107
|
+
if (clients[uuid]) {
|
|
108
|
+
clients[uuid].end();
|
|
109
|
+
}
|
|
110
|
+
return;
|
|
79
111
|
}
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
112
|
|
|
83
|
-
|
|
113
|
+
const client = clients[uuid] || createTcpClient(targetUrl, targetPort, ws, tunnelId, uuid);
|
|
114
|
+
|
|
115
|
+
if (!client.write(payload)) {
|
|
116
|
+
logger.debug(`Backpressure on TCP socket for uuid=${uuid}`);
|
|
117
|
+
client.once('drain', () => {
|
|
118
|
+
logger.info(`TCP socket drained for uuid=${uuid}`);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
84
122
|
|
|
85
|
-
if (
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
123
|
+
} else if (type === MESSAGE_TYPE_APP_PONG) {
|
|
124
|
+
try {
|
|
125
|
+
const pongData = JSON.parse(payload.toString());
|
|
126
|
+
// Accetta solo pong con seq >= pingSeq - 10 (finestra di 10 ping)
|
|
127
|
+
if (pongData.seq >= (pingSeq - 10)) {
|
|
128
|
+
lastPongTs = Date.now();
|
|
129
|
+
logger.trace(`App pong received: seq=${pongData.seq}`);
|
|
130
|
+
} else {
|
|
131
|
+
logger.debug(`Ignoring old pong: seq=${pongData.seq}`);
|
|
132
|
+
}
|
|
133
|
+
} catch (err) {
|
|
134
|
+
logger.error(`Invalid app pong format: ${err.message}`);
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
90
137
|
}
|
|
91
138
|
}
|
|
92
139
|
});
|
|
@@ -95,6 +142,8 @@ function connectWebSocket(config) {
|
|
|
95
142
|
logger.warn('WebSocket connection closed. Cleaning up clients.');
|
|
96
143
|
eventEmitter.emit('disconnected');
|
|
97
144
|
clearInterval(pingInterval);
|
|
145
|
+
clearInterval(appPingInterval);
|
|
146
|
+
clearInterval(healthMonitor);
|
|
98
147
|
|
|
99
148
|
for (const uuid in clients) {
|
|
100
149
|
logger.debug(`Closing TCP connection for uuid=${uuid}`);
|
|
@@ -104,8 +153,12 @@ function connectWebSocket(config) {
|
|
|
104
153
|
}
|
|
105
154
|
|
|
106
155
|
if (!isClosed && autoReconnect) {
|
|
107
|
-
|
|
108
|
-
|
|
156
|
+
const delay = RECONNECT_BACKOFF[reconnectAttempt] || RECONNECT_BACKOFF[RECONNECT_BACKOFF.length - 1];
|
|
157
|
+
logger.info(`Reconnecting in ${delay / 1000}s (attempt ${reconnectAttempt + 1})`);
|
|
158
|
+
setTimeout(() => {
|
|
159
|
+
reconnectAttempt = Math.min(reconnectAttempt + 1, RECONNECT_BACKOFF.length);
|
|
160
|
+
connect();
|
|
161
|
+
}, delay);
|
|
109
162
|
}
|
|
110
163
|
});
|
|
111
164
|
|
|
@@ -151,7 +204,7 @@ function heartBeat(ws) {
|
|
|
151
204
|
}
|
|
152
205
|
|
|
153
206
|
/**
|
|
154
|
-
* Creates a TCP connection to
|
|
207
|
+
* Creates a TCP connection to target service.
|
|
155
208
|
*/
|
|
156
209
|
function createTcpClient(targetUrl, targetPort, ws, tunnelId, uuid) {
|
|
157
210
|
const hostname = new URL(targetUrl).hostname;
|
|
@@ -184,6 +237,42 @@ function createTcpClient(targetUrl, targetPort, ws, tunnelId, uuid) {
|
|
|
184
237
|
return client;
|
|
185
238
|
}
|
|
186
239
|
|
|
240
|
+
/**
|
|
241
|
+
* Starts the application-level heartbeat (ping every 20 seconds)
|
|
242
|
+
*/
|
|
243
|
+
function startAppHeartbeat(ws, tunnelId, pingState) {
|
|
244
|
+
return setInterval(() => {
|
|
245
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
246
|
+
pingState.incPingSeq();
|
|
247
|
+
const currentPingSeq = pingState.pingSeq();
|
|
248
|
+
const pingData = JSON.stringify({
|
|
249
|
+
type: 'ping',
|
|
250
|
+
seq: currentPingSeq,
|
|
251
|
+
ts: Date.now()
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const message = buildMessageBuffer(tunnelId, uuidv4(), MESSAGE_TYPE_APP_PING, pingData);
|
|
255
|
+
ws.send(message);
|
|
256
|
+
|
|
257
|
+
logger.trace(`App ping sent: seq=${currentPingSeq}`);
|
|
258
|
+
}
|
|
259
|
+
}, APP_PING_INTERVAL);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Starts health monitoring with sliding window timeout
|
|
264
|
+
*/
|
|
265
|
+
function startHealthMonitor(ws, tunnelId, pongState) {
|
|
266
|
+
return setInterval(() => {
|
|
267
|
+
const now = Date.now();
|
|
268
|
+
const currentLastPongTs = pongState.lastPongTs();
|
|
269
|
+
if (now - currentLastPongTs > HEALTH_TIMEOUT) {
|
|
270
|
+
logger.warn(`Health timeout exceeded (${HEALTH_TIMEOUT}ms) - terminating connection`);
|
|
271
|
+
ws.terminate();
|
|
272
|
+
}
|
|
273
|
+
}, 5000); // Check every 5 seconds
|
|
274
|
+
}
|
|
275
|
+
|
|
187
276
|
function resetClients() { // for testing
|
|
188
277
|
for (const key in clients) {
|
|
189
278
|
delete clients[key];
|
|
@@ -193,4 +282,4 @@ function resetClients() { // for testing
|
|
|
193
282
|
module.exports = {
|
|
194
283
|
connectWebSocket,
|
|
195
284
|
resetClients, // for testing
|
|
196
|
-
};
|
|
285
|
+
};
|
package/package.json
CHANGED
package/server/constants.js
CHANGED
package/server/messageHandler.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
const state = require('./state');
|
|
2
|
-
const { MESSAGE_TYPE_CONFIG, MESSAGE_TYPE_DATA } = require('./constants');
|
|
2
|
+
const { MESSAGE_TYPE_CONFIG, MESSAGE_TYPE_DATA, MESSAGE_TYPE_APP_PING, MESSAGE_TYPE_APP_PONG } = require('./constants');
|
|
3
3
|
const { startTCPServer } = require('./tcpServer');
|
|
4
4
|
const { logger } = require('../utils/logger');
|
|
5
|
+
const { buildMessageBuffer } = require('../client/utils');
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Handles a parsed WebSocket message.
|
|
@@ -65,6 +66,25 @@ function handleParsedMessage(ws, tunnelId, uuid, type, payload, tunnelIdHeaderNa
|
|
|
65
66
|
return;
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
// Handle MESSAGE_TYPE_APP_PING
|
|
70
|
+
if (type === MESSAGE_TYPE_APP_PING) {
|
|
71
|
+
try {
|
|
72
|
+
const pingData = JSON.parse(payload.toString());
|
|
73
|
+
const pongData = JSON.stringify({
|
|
74
|
+
type: 'pong',
|
|
75
|
+
seq: pingData.seq
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const pongMessage = buildMessageBuffer(tunnelId, uuid, MESSAGE_TYPE_APP_PONG, pongData);
|
|
79
|
+
ws.send(pongMessage);
|
|
80
|
+
|
|
81
|
+
logger.trace(`App pong sent: seq=${pingData.seq} for tunnel ${tunnelId}`);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
logger.error(`Invalid app ping format for tunnel ${tunnelId}: ${err.message}`);
|
|
84
|
+
}
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
68
88
|
// Handle MESSAGE_TYPE_DATA
|
|
69
89
|
const tunnel = state[port]?.websocketTunnels?.[tunnelId];
|
|
70
90
|
|
package/server/tcpServer.js
CHANGED
|
@@ -5,6 +5,7 @@ const { HTTPParser, methods } = require('http-parser-js');
|
|
|
5
5
|
const state = require('./state');
|
|
6
6
|
const { MESSAGE_TYPE_DATA } = require('./constants');
|
|
7
7
|
const { logger } = require('../utils/logger');
|
|
8
|
+
const { buildMessageBuffer } = require('../client/utils');
|
|
8
9
|
|
|
9
10
|
function startTCPServer(port, tunnelIdHeaderName, websocketPort) {
|
|
10
11
|
const wsPortKey = String(websocketPort);
|
|
@@ -60,7 +61,8 @@ function startTCPServer(port, tunnelIdHeaderName, websocketPort) {
|
|
|
60
61
|
isWebSocket = headers['upgrade']?.toLowerCase() === 'websocket';
|
|
61
62
|
|
|
62
63
|
logger.trace(`Sending initial headers (${rawHeaders.length} bytes) to tunnel [${currentTunnelId}]`);
|
|
63
|
-
|
|
64
|
+
const message = buildMessageBuffer(currentTunnelId, uuid, MESSAGE_TYPE_DATA, rawHeaders);
|
|
65
|
+
tunnel.ws.send(message);
|
|
64
66
|
|
|
65
67
|
if (isWebSocket) parser.close();
|
|
66
68
|
};
|
|
@@ -70,7 +72,8 @@ function startTCPServer(port, tunnelIdHeaderName, websocketPort) {
|
|
|
70
72
|
if (tunnel?.ws && !isWebSocket) {
|
|
71
73
|
const body = chunk.slice(offset, offset + length);
|
|
72
74
|
logger.trace(`Forwarding body (${body.length} bytes) to tunnel [${currentTunnelId}]`);
|
|
73
|
-
|
|
75
|
+
const message = buildMessageBuffer(currentTunnelId, uuid, MESSAGE_TYPE_DATA, body);
|
|
76
|
+
tunnel.ws.send(message);
|
|
74
77
|
}
|
|
75
78
|
};
|
|
76
79
|
|
|
@@ -91,7 +94,8 @@ function startTCPServer(port, tunnelIdHeaderName, websocketPort) {
|
|
|
91
94
|
if (isWebSocket) {
|
|
92
95
|
if (tunnel?.ws) {
|
|
93
96
|
logger.trace(`Forwarding WebSocket TCP data (${chunk.length} bytes) for tunnel [${currentTunnelId}]`);
|
|
94
|
-
|
|
97
|
+
const message = buildMessageBuffer(currentTunnelId, uuid, MESSAGE_TYPE_DATA, chunk);
|
|
98
|
+
tunnel.ws.send(message);
|
|
95
99
|
}
|
|
96
100
|
} else {
|
|
97
101
|
try {
|
|
@@ -107,7 +111,8 @@ function startTCPServer(port, tunnelIdHeaderName, websocketPort) {
|
|
|
107
111
|
const tunnel = state[wsPortKey]?.websocketTunnels?.[currentTunnelId];
|
|
108
112
|
if (tunnel?.ws) {
|
|
109
113
|
logger.debug(`TCP socket end for tunnel [${currentTunnelId}] (uuid: ${uuid})`);
|
|
110
|
-
|
|
114
|
+
const message = buildMessageBuffer(currentTunnelId, uuid, MESSAGE_TYPE_DATA, 'CLOSE');
|
|
115
|
+
tunnel.ws.send(message);
|
|
111
116
|
}
|
|
112
117
|
});
|
|
113
118
|
|