@remotelinker/reverse-ws-tunnel 1.0.8 â 1.0.10
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 +24 -0
- package/client/index.js +11 -1
- package/client/index.mjs +1 -1
- package/client/proxyServer.js +23 -12
- package/client/tunnelClient.js +122 -47
- package/client/utils.js +2 -1
- package/index.cjs +2 -2
- package/index.mjs +1 -1
- package/package.json +5 -2
- package/server/index.mjs +1 -1
- package/server/messageHandler.js +13 -4
- package/server/tcpServer.js +25 -13
- package/server/websocketServer.js +25 -10
- package/utils/index.mjs +1 -1
- package/utils/loadConfig.js +9 -3
- package/utils/logger.js +2 -2
package/README.md
CHANGED
|
@@ -30,6 +30,30 @@ Reverse WebSocket Tunnel is a library that enables you to expose local services
|
|
|
30
30
|
|
|
31
31
|
---
|
|
32
32
|
|
|
33
|
+
## ⨠v1.0.10 - What's New
|
|
34
|
+
|
|
35
|
+
### đ§ Code Quality & Developer Experience
|
|
36
|
+
- **Code Cleanup**: Removed unused constants and redundant variables
|
|
37
|
+
- **Input Validation**: Added tunnelId validation for incoming messages
|
|
38
|
+
- **Code Formatting**: Added Prettier configuration for consistent code style
|
|
39
|
+
- **Test Suite**: Reorganized tests, removed obsolete files, added new test coverage
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## ⨠v1.0.9 - Previous Release
|
|
44
|
+
|
|
45
|
+
### đ Bug Fixes
|
|
46
|
+
- **Message Format Standardization**: Fixed inconsistent message formats between server components
|
|
47
|
+
- **Ping/Pong Reliability**: Resolved issues with application-level heartbeat failing during data transfer
|
|
48
|
+
- **Connection Stability**: Improved connection handling and reduced timeout issues
|
|
49
|
+
|
|
50
|
+
### đ§ Technical Improvements
|
|
51
|
+
- **Unified Message Protocol**: All server messages now use consistent `buildMessageBuffer` format
|
|
52
|
+
- **Simplified Client Architecture**: Removed hybrid parsing logic for better maintainability
|
|
53
|
+
- **Enhanced Buffer Management**: Improved message buffering and parsing reliability
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
33
57
|
## đĻ Installation
|
|
34
58
|
|
|
35
59
|
```bash
|
package/client/index.js
CHANGED
|
@@ -2,7 +2,17 @@ const { startHttpProxyServer } = require('./proxyServer');
|
|
|
2
2
|
const { connectWebSocket } = require('./tunnelClient');
|
|
3
3
|
const { setLogContext } = require('../utils/logger');
|
|
4
4
|
|
|
5
|
-
function startClient({
|
|
5
|
+
function startClient({
|
|
6
|
+
targetUrl,
|
|
7
|
+
allowInsicureCerts,
|
|
8
|
+
wsUrl,
|
|
9
|
+
tunnelId,
|
|
10
|
+
tunnelEntryUrl,
|
|
11
|
+
tunnelEntryPort,
|
|
12
|
+
headers,
|
|
13
|
+
environment,
|
|
14
|
+
autoReconnect,
|
|
15
|
+
}) {
|
|
6
16
|
setLogContext('CLIENT');
|
|
7
17
|
environment = environment || 'production';
|
|
8
18
|
const proxy = startHttpProxyServer(targetUrl, allowInsicureCerts);
|
package/client/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import cjsModule from './index.js';
|
|
2
|
-
export const { startClient } = cjsModule;
|
|
2
|
+
export const { startClient } = cjsModule;
|
package/client/proxyServer.js
CHANGED
|
@@ -24,23 +24,34 @@ function startHttpProxyServer(targetUrl, allowInsecureCerts = false) {
|
|
|
24
24
|
return res.end('Missing TARGET_URL');
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
proxy.web(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
res.
|
|
27
|
+
proxy.web(
|
|
28
|
+
req,
|
|
29
|
+
res,
|
|
30
|
+
{ target: targetUrl, changeOrigin: true, secure: !allowInsecureCerts },
|
|
31
|
+
err => {
|
|
32
|
+
logger.error('Proxy web error:', err);
|
|
33
|
+
if (!res.headersSent) {
|
|
34
|
+
res.writeHead(502);
|
|
35
|
+
res.end('Bad gateway');
|
|
36
|
+
} else {
|
|
37
|
+
res.end();
|
|
38
|
+
}
|
|
34
39
|
}
|
|
35
|
-
|
|
40
|
+
);
|
|
36
41
|
});
|
|
37
42
|
|
|
38
43
|
server.on('upgrade', (req, socket, head) => {
|
|
39
44
|
logger.trace(`Incoming WebSocket upgrade: ${req.url}`);
|
|
40
|
-
proxy.ws(
|
|
41
|
-
|
|
42
|
-
socket
|
|
43
|
-
|
|
45
|
+
proxy.ws(
|
|
46
|
+
req,
|
|
47
|
+
socket,
|
|
48
|
+
head,
|
|
49
|
+
{ target: targetUrl, changeOrigin: false, secure: !allowInsecureCerts },
|
|
50
|
+
err => {
|
|
51
|
+
logger.error('Proxy WS upgrade error:', err);
|
|
52
|
+
socket.end();
|
|
53
|
+
}
|
|
54
|
+
);
|
|
44
55
|
});
|
|
45
56
|
|
|
46
57
|
proxy.on('error', (err, req, res) => {
|
package/client/tunnelClient.js
CHANGED
|
@@ -6,7 +6,6 @@ const { buildMessageBuffer } = require('./utils');
|
|
|
6
6
|
const { logger } = require('../utils/logger');
|
|
7
7
|
const packageJson = require('../package.json');
|
|
8
8
|
|
|
9
|
-
const RECONNECT_INTERVAL = 5000;
|
|
10
9
|
const MESSAGE_TYPE_CONFIG = 0x01;
|
|
11
10
|
const MESSAGE_TYPE_DATA = 0x02;
|
|
12
11
|
const MESSAGE_TYPE_APP_PING = 0x03;
|
|
@@ -19,11 +18,21 @@ const HEALTH_TIMEOUT = 45 * 1000; // 45 secondi sliding window
|
|
|
19
18
|
const RECONNECT_BACKOFF = [1000, 2000, 5000, 10000, 30000]; // Backoff progressivo
|
|
20
19
|
|
|
21
20
|
/**
|
|
22
|
-
* Starts
|
|
21
|
+
* Starts WebSocket tunnel client.
|
|
23
22
|
* @param {Object} config - Configuration for tunnel.
|
|
24
23
|
*/
|
|
25
24
|
function connectWebSocket(config) {
|
|
26
|
-
const {
|
|
25
|
+
const {
|
|
26
|
+
wsUrl,
|
|
27
|
+
tunnelId,
|
|
28
|
+
targetUrl,
|
|
29
|
+
targetPort,
|
|
30
|
+
tunnelEntryUrl,
|
|
31
|
+
tunnelEntryPort,
|
|
32
|
+
headers,
|
|
33
|
+
environment,
|
|
34
|
+
autoReconnect = true,
|
|
35
|
+
} = config;
|
|
27
36
|
|
|
28
37
|
const eventEmitter = new EventEmitter();
|
|
29
38
|
let ws;
|
|
@@ -32,8 +41,6 @@ function connectWebSocket(config) {
|
|
|
32
41
|
let healthMonitor;
|
|
33
42
|
let isClosed = false;
|
|
34
43
|
let reconnectAttempt = 0;
|
|
35
|
-
let pingSeq = 0;
|
|
36
|
-
let lastPongTs = Date.now();
|
|
37
44
|
|
|
38
45
|
if (!tunnelId) {
|
|
39
46
|
throw new Error(`Missing mandatory tunnelId`);
|
|
@@ -43,25 +50,59 @@ function connectWebSocket(config) {
|
|
|
43
50
|
if (isClosed) return;
|
|
44
51
|
|
|
45
52
|
try {
|
|
46
|
-
|
|
53
|
+
// Parse headers - handle both string and object formats
|
|
54
|
+
let headersParsed = {};
|
|
55
|
+
if (headers) {
|
|
56
|
+
if (typeof headers === 'string') {
|
|
57
|
+
try {
|
|
58
|
+
headersParsed = JSON.parse(headers);
|
|
59
|
+
} catch (e) {
|
|
60
|
+
logger.warn(`Failed to parse headers string: ${headers}`);
|
|
61
|
+
}
|
|
62
|
+
} else if (typeof headers === 'object') {
|
|
63
|
+
headersParsed = headers;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
47
66
|
logger.debug(`Parsed headers: ${JSON.stringify(headersParsed)}`);
|
|
48
67
|
logger.debug(`Try to connect to: ${wsUrl}`);
|
|
49
68
|
ws = new WebSocket(wsUrl, { headers: headersParsed });
|
|
50
69
|
logger.debug(`Connection: ${wsUrl}`);
|
|
51
70
|
} catch (error) {
|
|
52
|
-
logger.error('
|
|
71
|
+
logger.error('Failed to create WebSocket connection:', error);
|
|
53
72
|
return;
|
|
54
73
|
}
|
|
55
74
|
|
|
75
|
+
// PingState condiviso tra heartbeat e message handler
|
|
76
|
+
// Reset completo dello stato per ogni connessione
|
|
77
|
+
const pingState = {
|
|
78
|
+
pingSeq: 0,
|
|
79
|
+
lastPongTs: Date.now(),
|
|
80
|
+
};
|
|
81
|
+
const pingStateCallbacks = {
|
|
82
|
+
pingSeq: () => pingState.pingSeq,
|
|
83
|
+
incPingSeq: () => pingState.pingSeq++,
|
|
84
|
+
lastPongTs: () => pingState.lastPongTs,
|
|
85
|
+
setLastPongTs: ts => (pingState.lastPongTs = ts),
|
|
86
|
+
};
|
|
87
|
+
|
|
56
88
|
ws.on('open', () => {
|
|
57
89
|
logger.info(`Connected to WebSocket server ${wsUrl}`);
|
|
58
|
-
logger.warn(
|
|
90
|
+
logger.warn(
|
|
91
|
+
`WS tunnel config sent: TARGET_PORT=${targetPort}, ENTRY_PORT=${tunnelEntryPort}`
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// Reset reconnect attempt on successful connection
|
|
95
|
+
reconnectAttempt = 0;
|
|
96
|
+
|
|
59
97
|
eventEmitter.emit('connected');
|
|
60
98
|
({ pingInterval } = heartBeat(ws));
|
|
61
99
|
|
|
62
100
|
// Avviare heartbeat applicativo
|
|
63
|
-
appPingInterval = startAppHeartbeat(ws, tunnelId,
|
|
64
|
-
healthMonitor = startHealthMonitor(ws, tunnelId, {
|
|
101
|
+
appPingInterval = startAppHeartbeat(ws, tunnelId, pingStateCallbacks);
|
|
102
|
+
healthMonitor = startHealthMonitor(ws, tunnelId, {
|
|
103
|
+
lastPongTs: () => pingState.lastPongTs,
|
|
104
|
+
setLastPongTs: ts => (pingState.lastPongTs = ts),
|
|
105
|
+
});
|
|
65
106
|
|
|
66
107
|
const uuid = uuidv4();
|
|
67
108
|
const payload = {
|
|
@@ -73,57 +114,86 @@ function connectWebSocket(config) {
|
|
|
73
114
|
agentVersion: packageJson.version,
|
|
74
115
|
};
|
|
75
116
|
|
|
76
|
-
const message = buildMessageBuffer(
|
|
117
|
+
const message = buildMessageBuffer(
|
|
118
|
+
tunnelId,
|
|
119
|
+
uuid,
|
|
120
|
+
MESSAGE_TYPE_CONFIG,
|
|
121
|
+
JSON.stringify(payload)
|
|
122
|
+
);
|
|
77
123
|
logger.debug(`Sending tunnel config [uuid=${uuid}]`);
|
|
78
124
|
ws.send(message);
|
|
79
125
|
});
|
|
80
126
|
|
|
81
127
|
let messageBuffer = Buffer.alloc(0);
|
|
82
|
-
|
|
83
|
-
ws.on('message',
|
|
84
|
-
logger.trace(`Received message chunk: ${
|
|
85
|
-
messageBuffer = Buffer.concat([messageBuffer,
|
|
128
|
+
|
|
129
|
+
ws.on('message', data => {
|
|
130
|
+
logger.trace(`Received message chunk: ${data.length} bytes`);
|
|
131
|
+
messageBuffer = Buffer.concat([messageBuffer, data]);
|
|
86
132
|
|
|
87
133
|
while (messageBuffer.length >= 4) {
|
|
88
134
|
const length = messageBuffer.readUInt32BE(0);
|
|
89
|
-
if (messageBuffer.length < 4 + length)
|
|
135
|
+
if (messageBuffer.length < 4 + length) {
|
|
136
|
+
logger.trace(
|
|
137
|
+
`Waiting for more data: need ${4 + length} bytes, have ${messageBuffer.length}`
|
|
138
|
+
);
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
90
141
|
|
|
91
142
|
const message = messageBuffer.slice(4, 4 + length);
|
|
92
143
|
messageBuffer = messageBuffer.slice(4 + length);
|
|
93
144
|
|
|
94
|
-
const messageTunnelId = message.slice(0, 36).toString();
|
|
145
|
+
const messageTunnelId = message.slice(0, 36).toString().trim();
|
|
95
146
|
const uuid = message.slice(36, 72).toString();
|
|
96
147
|
const type = message.readUInt8(72);
|
|
97
148
|
const payload = message.slice(73);
|
|
98
149
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (clients[uuid]) {
|
|
105
|
-
clients[uuid].end();
|
|
106
|
-
}
|
|
150
|
+
// Validate tunnelId matches expected tunnel
|
|
151
|
+
if (messageTunnelId !== tunnelId) {
|
|
152
|
+
logger.warn(
|
|
153
|
+
`Received message for wrong tunnel: ${messageTunnelId} (expected: ${tunnelId})`
|
|
154
|
+
);
|
|
107
155
|
return;
|
|
108
156
|
}
|
|
109
157
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
158
|
+
logger.trace(
|
|
159
|
+
`Received WS message for uuid=${uuid}, type=${type}, length=${payload.length}`
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
if (type === MESSAGE_TYPE_DATA) {
|
|
163
|
+
if (payload.toString() === 'CLOSE') {
|
|
164
|
+
logger.debug(`Received CLOSE for uuid=${uuid}`);
|
|
165
|
+
if (clients[uuid]) {
|
|
166
|
+
clients[uuid].end();
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
121
169
|
}
|
|
122
|
-
|
|
123
|
-
|
|
170
|
+
|
|
171
|
+
const client =
|
|
172
|
+
clients[uuid] || createTcpClient(targetUrl, targetPort, ws, tunnelId, uuid);
|
|
173
|
+
|
|
174
|
+
if (!client.write(payload)) {
|
|
175
|
+
logger.debug(`Backpressure on TCP socket for uuid=${uuid}`);
|
|
176
|
+
client.once('drain', () => {
|
|
177
|
+
logger.info(`TCP socket drained for uuid=${uuid}`);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return;
|
|
181
|
+
} else if (type === MESSAGE_TYPE_APP_PONG) {
|
|
182
|
+
try {
|
|
183
|
+
const pongData = JSON.parse(payload.toString());
|
|
184
|
+
// Accetta solo pong con seq >= pingSeq - 10 (finestra di 10 ping)
|
|
185
|
+
if (pongData.seq >= pingStateCallbacks.pingSeq() - 10) {
|
|
186
|
+
// Aggiorna lastPongTs usando il callback
|
|
187
|
+
pingStateCallbacks.setLastPongTs(Date.now());
|
|
188
|
+
logger.trace(`App pong received: seq=${pongData.seq}`);
|
|
189
|
+
} else {
|
|
190
|
+
logger.debug(`Ignoring old pong: seq=${pongData.seq}`);
|
|
191
|
+
}
|
|
192
|
+
} catch (err) {
|
|
193
|
+
logger.error(`Invalid app pong format: ${err.message}`);
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
124
196
|
}
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
197
|
}
|
|
128
198
|
});
|
|
129
199
|
|
|
@@ -141,8 +211,12 @@ function connectWebSocket(config) {
|
|
|
141
211
|
delete clients[uuid];
|
|
142
212
|
}
|
|
143
213
|
|
|
214
|
+
// Reset message buffer on close for proper reconnection
|
|
215
|
+
messageBuffer = Buffer.alloc(0);
|
|
216
|
+
|
|
144
217
|
if (!isClosed && autoReconnect) {
|
|
145
|
-
const delay =
|
|
218
|
+
const delay =
|
|
219
|
+
RECONNECT_BACKOFF[reconnectAttempt] || RECONNECT_BACKOFF[RECONNECT_BACKOFF.length - 1];
|
|
146
220
|
logger.info(`Reconnecting in ${delay / 1000}s (attempt ${reconnectAttempt + 1})`);
|
|
147
221
|
setTimeout(() => {
|
|
148
222
|
reconnectAttempt = Math.min(reconnectAttempt + 1, RECONNECT_BACKOFF.length);
|
|
@@ -151,7 +225,7 @@ function connectWebSocket(config) {
|
|
|
151
225
|
}
|
|
152
226
|
});
|
|
153
227
|
|
|
154
|
-
ws.on('error',
|
|
228
|
+
ws.on('error', err => {
|
|
155
229
|
logger.error('WebSocket error:', err);
|
|
156
230
|
});
|
|
157
231
|
};
|
|
@@ -193,7 +267,7 @@ function heartBeat(ws) {
|
|
|
193
267
|
}
|
|
194
268
|
|
|
195
269
|
/**
|
|
196
|
-
* Creates a TCP connection to
|
|
270
|
+
* Creates a TCP connection to target service.
|
|
197
271
|
*/
|
|
198
272
|
function createTcpClient(targetUrl, targetPort, ws, tunnelId, uuid) {
|
|
199
273
|
const hostname = new URL(targetUrl).hostname;
|
|
@@ -206,13 +280,13 @@ function createTcpClient(targetUrl, targetPort, ws, tunnelId, uuid) {
|
|
|
206
280
|
logger.info(`TCP connection established for uuid=${uuid}`);
|
|
207
281
|
});
|
|
208
282
|
|
|
209
|
-
client.on('data',
|
|
283
|
+
client.on('data', data => {
|
|
210
284
|
logger.trace(`TCP data received for uuid=${uuid}, length=${data.length}`);
|
|
211
285
|
const message = buildMessageBuffer(tunnelId, uuid, MESSAGE_TYPE_DATA, data);
|
|
212
286
|
ws.send(message);
|
|
213
287
|
});
|
|
214
288
|
|
|
215
|
-
client.on('error',
|
|
289
|
+
client.on('error', err => {
|
|
216
290
|
logger.error(`TCP error for uuid=${uuid}:`, err);
|
|
217
291
|
client.destroy();
|
|
218
292
|
delete clients[uuid];
|
|
@@ -237,7 +311,7 @@ function startAppHeartbeat(ws, tunnelId, pingState) {
|
|
|
237
311
|
const pingData = JSON.stringify({
|
|
238
312
|
type: 'ping',
|
|
239
313
|
seq: currentPingSeq,
|
|
240
|
-
ts: Date.now()
|
|
314
|
+
ts: Date.now(),
|
|
241
315
|
});
|
|
242
316
|
|
|
243
317
|
const message = buildMessageBuffer(tunnelId, uuidv4(), MESSAGE_TYPE_APP_PING, pingData);
|
|
@@ -262,7 +336,8 @@ function startHealthMonitor(ws, tunnelId, pongState) {
|
|
|
262
336
|
}, 5000); // Check every 5 seconds
|
|
263
337
|
}
|
|
264
338
|
|
|
265
|
-
function resetClients() {
|
|
339
|
+
function resetClients() {
|
|
340
|
+
// for testing
|
|
266
341
|
for (const key in clients) {
|
|
267
342
|
delete clients[key];
|
|
268
343
|
}
|
package/client/utils.js
CHANGED
|
@@ -12,7 +12,8 @@ function buildMessageBuffer(tunnelId, uuid, type, payload) {
|
|
|
12
12
|
const typeBuffer = Buffer.from([type]);
|
|
13
13
|
const payloadBuffer = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, 'utf8');
|
|
14
14
|
|
|
15
|
-
const totalLength =
|
|
15
|
+
const totalLength =
|
|
16
|
+
tunnelBuffer.length + uuidBuffer.length + typeBuffer.length + payloadBuffer.length;
|
|
16
17
|
const lengthBuffer = Buffer.alloc(4);
|
|
17
18
|
lengthBuffer.writeUInt32BE(totalLength);
|
|
18
19
|
|
package/index.cjs
CHANGED
package/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remotelinker/reverse-ws-tunnel",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "A Node.js library for creating secure reverse tunnels over WebSocket connections",
|
|
5
5
|
"main": "index.cjs",
|
|
6
6
|
"types": "types/index.d.ts",
|
|
@@ -41,7 +41,9 @@
|
|
|
41
41
|
"example:server:esm": "node examples/server/server-example.mjs",
|
|
42
42
|
"example:client": "node examples/client/client-example.js",
|
|
43
43
|
"example:client:esm": "node examples/client/client-example.mjs",
|
|
44
|
-
"example:webserver": "node examples/webserver/webserver-example.js"
|
|
44
|
+
"example:webserver": "node examples/webserver/webserver-example.js",
|
|
45
|
+
"format": "prettier --write \"**/*.js\" \"**/*.mjs\" \"**/*.cjs\"",
|
|
46
|
+
"format:check": "prettier --check \"**/*.js\" \"**/*.mjs\" \"**/*.cjs\""
|
|
45
47
|
},
|
|
46
48
|
"exports": {
|
|
47
49
|
".": {
|
|
@@ -78,6 +80,7 @@
|
|
|
78
80
|
},
|
|
79
81
|
"devDependencies": {
|
|
80
82
|
"jest": "^29.7.0",
|
|
83
|
+
"prettier": "^3.8.1",
|
|
81
84
|
"typescript": "^5.9.3"
|
|
82
85
|
}
|
|
83
86
|
}
|
package/server/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import cjsModule from './index.js';
|
|
2
|
-
export const { startWebSocketServer, setLogContext } = cjsModule;
|
|
2
|
+
export const { startWebSocketServer, setLogContext } = cjsModule;
|
package/server/messageHandler.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
const state = require('./state');
|
|
2
|
-
const {
|
|
2
|
+
const {
|
|
3
|
+
MESSAGE_TYPE_CONFIG,
|
|
4
|
+
MESSAGE_TYPE_DATA,
|
|
5
|
+
MESSAGE_TYPE_APP_PING,
|
|
6
|
+
MESSAGE_TYPE_APP_PONG,
|
|
7
|
+
} = require('./constants');
|
|
3
8
|
const { startTCPServer } = require('./tcpServer');
|
|
4
9
|
const { logger } = require('../utils/logger');
|
|
5
10
|
const { buildMessageBuffer } = require('../client/utils');
|
|
@@ -49,7 +54,9 @@ function handleParsedMessage(ws, tunnelId, uuid, type, payload, tunnelIdHeaderNa
|
|
|
49
54
|
|
|
50
55
|
const portKey = String(TUNNEL_ENTRY_PORT);
|
|
51
56
|
if (!state[port][portKey]) {
|
|
52
|
-
logger.info(
|
|
57
|
+
logger.info(
|
|
58
|
+
`Starting new TCP server on port ${TUNNEL_ENTRY_PORT} for tunnelId=${tunnelId}`
|
|
59
|
+
);
|
|
53
60
|
state[port][portKey] = {};
|
|
54
61
|
state[port][portKey] = {
|
|
55
62
|
tcpServer: startTCPServer(TUNNEL_ENTRY_PORT, tunnelIdHeaderName, port),
|
|
@@ -60,7 +67,9 @@ function handleParsedMessage(ws, tunnelId, uuid, type, payload, tunnelIdHeaderNa
|
|
|
60
67
|
|
|
61
68
|
logger.info(`Tunnel [${tunnelId}] established successfully`);
|
|
62
69
|
} catch (error) {
|
|
63
|
-
logger.error(
|
|
70
|
+
logger.error(
|
|
71
|
+
`Failed to process MESSAGE_TYPE_CONFIG for tunnelId=${tunnelId}: ${error.message}`
|
|
72
|
+
);
|
|
64
73
|
}
|
|
65
74
|
|
|
66
75
|
return;
|
|
@@ -72,7 +81,7 @@ function handleParsedMessage(ws, tunnelId, uuid, type, payload, tunnelIdHeaderNa
|
|
|
72
81
|
const pingData = JSON.parse(payload.toString());
|
|
73
82
|
const pongData = JSON.stringify({
|
|
74
83
|
type: 'pong',
|
|
75
|
-
seq: pingData.seq
|
|
84
|
+
seq: pingData.seq,
|
|
76
85
|
});
|
|
77
86
|
|
|
78
87
|
const pongMessage = buildMessageBuffer(tunnelId, uuid, MESSAGE_TYPE_APP_PONG, pongData);
|
package/server/tcpServer.js
CHANGED
|
@@ -5,12 +5,13 @@ 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);
|
|
11
12
|
const tcpPortKey = String(port);
|
|
12
13
|
|
|
13
|
-
const server = net.createServer(
|
|
14
|
+
const server = net.createServer(socket => {
|
|
14
15
|
const uuid = uuidv4();
|
|
15
16
|
const uuidBuffer = Buffer.from(uuid);
|
|
16
17
|
let currentTunnelId = null;
|
|
@@ -21,7 +22,7 @@ function startTCPServer(port, tunnelIdHeaderName, websocketPort) {
|
|
|
21
22
|
function createParser() {
|
|
22
23
|
const parser = new HTTPParser(HTTPParser.REQUEST);
|
|
23
24
|
|
|
24
|
-
parser[HTTPParser.kOnHeadersComplete] =
|
|
25
|
+
parser[HTTPParser.kOnHeadersComplete] = info => {
|
|
25
26
|
const headers = info.headers.reduce((acc, val, i, arr) => {
|
|
26
27
|
if (i % 2 === 0) acc[val.toLowerCase()] = arr[i + 1];
|
|
27
28
|
return acc;
|
|
@@ -59,8 +60,11 @@ function startTCPServer(port, tunnelIdHeaderName, websocketPort) {
|
|
|
59
60
|
|
|
60
61
|
isWebSocket = headers['upgrade']?.toLowerCase() === 'websocket';
|
|
61
62
|
|
|
62
|
-
logger.trace(
|
|
63
|
-
|
|
63
|
+
logger.trace(
|
|
64
|
+
`Sending initial headers (${rawHeaders.length} bytes) to tunnel [${currentTunnelId}]`
|
|
65
|
+
);
|
|
66
|
+
const message = buildMessageBuffer(currentTunnelId, uuid, MESSAGE_TYPE_DATA, rawHeaders);
|
|
67
|
+
tunnel.ws.send(message);
|
|
64
68
|
|
|
65
69
|
if (isWebSocket) parser.close();
|
|
66
70
|
};
|
|
@@ -70,7 +74,8 @@ function startTCPServer(port, tunnelIdHeaderName, websocketPort) {
|
|
|
70
74
|
if (tunnel?.ws && !isWebSocket) {
|
|
71
75
|
const body = chunk.slice(offset, offset + length);
|
|
72
76
|
logger.trace(`Forwarding body (${body.length} bytes) to tunnel [${currentTunnelId}]`);
|
|
73
|
-
|
|
77
|
+
const message = buildMessageBuffer(currentTunnelId, uuid, MESSAGE_TYPE_DATA, body);
|
|
78
|
+
tunnel.ws.send(message);
|
|
74
79
|
}
|
|
75
80
|
};
|
|
76
81
|
|
|
@@ -86,12 +91,15 @@ function startTCPServer(port, tunnelIdHeaderName, websocketPort) {
|
|
|
86
91
|
|
|
87
92
|
let currentParser = createParser();
|
|
88
93
|
|
|
89
|
-
socket.on('data',
|
|
94
|
+
socket.on('data', chunk => {
|
|
90
95
|
const tunnel = state[wsPortKey]?.websocketTunnels?.[currentTunnelId];
|
|
91
96
|
if (isWebSocket) {
|
|
92
97
|
if (tunnel?.ws) {
|
|
93
|
-
logger.trace(
|
|
94
|
-
|
|
98
|
+
logger.trace(
|
|
99
|
+
`Forwarding WebSocket TCP data (${chunk.length} bytes) for tunnel [${currentTunnelId}]`
|
|
100
|
+
);
|
|
101
|
+
const message = buildMessageBuffer(currentTunnelId, uuid, MESSAGE_TYPE_DATA, chunk);
|
|
102
|
+
tunnel.ws.send(message);
|
|
95
103
|
}
|
|
96
104
|
} else {
|
|
97
105
|
try {
|
|
@@ -107,16 +115,20 @@ function startTCPServer(port, tunnelIdHeaderName, websocketPort) {
|
|
|
107
115
|
const tunnel = state[wsPortKey]?.websocketTunnels?.[currentTunnelId];
|
|
108
116
|
if (tunnel?.ws) {
|
|
109
117
|
logger.debug(`TCP socket end for tunnel [${currentTunnelId}] (uuid: ${uuid})`);
|
|
110
|
-
|
|
118
|
+
const message = buildMessageBuffer(currentTunnelId, uuid, MESSAGE_TYPE_DATA, 'CLOSE');
|
|
119
|
+
tunnel.ws.send(message);
|
|
111
120
|
}
|
|
112
121
|
});
|
|
113
122
|
|
|
114
123
|
socket.on('close', () => {
|
|
115
|
-
const deleted =
|
|
116
|
-
|
|
124
|
+
const deleted =
|
|
125
|
+
delete state[wsPortKey]?.websocketTunnels?.[currentTunnelId]?.tcpConnections?.[uuid];
|
|
126
|
+
logger.debug(
|
|
127
|
+
`TCP socket closed [${uuid}] for tunnel [${currentTunnelId}], connection ${deleted ? 'removed' : 'not found'}`
|
|
128
|
+
);
|
|
117
129
|
});
|
|
118
130
|
|
|
119
|
-
socket.on('error',
|
|
131
|
+
socket.on('error', err => {
|
|
120
132
|
logger.error(`Socket error on tunnel [${currentTunnelId}], uuid [${uuid}]:`, err);
|
|
121
133
|
delete state[wsPortKey]?.websocketTunnels?.[currentTunnelId]?.tcpConnections?.[uuid];
|
|
122
134
|
});
|
|
@@ -129,7 +141,7 @@ function startTCPServer(port, tunnelIdHeaderName, websocketPort) {
|
|
|
129
141
|
logger.info(`TCP server listening on port ${port} for websocketPort ${websocketPort}`);
|
|
130
142
|
});
|
|
131
143
|
|
|
132
|
-
server.on('error',
|
|
144
|
+
server.on('error', err => {
|
|
133
145
|
logger.error(`TCP server error on port ${port}:`, err);
|
|
134
146
|
});
|
|
135
147
|
}
|
|
@@ -22,7 +22,9 @@ function startWebSocketServer({ port, host, path, tunnelIdHeaderName }) {
|
|
|
22
22
|
state[portKey].webSocketServer = new WebSocket.Server({ port, host, path });
|
|
23
23
|
|
|
24
24
|
state[portKey].webSocketServer.on('listening', () => {
|
|
25
|
-
logger.info(
|
|
25
|
+
logger.info(
|
|
26
|
+
`WebSocket server listening on port ${port}${host ? ` (host: ${host})` : ''}${path ? `, path: ${path}` : ''}`
|
|
27
|
+
);
|
|
26
28
|
});
|
|
27
29
|
|
|
28
30
|
state[portKey].webSocketServer.on('connection', (ws, req) => {
|
|
@@ -41,7 +43,9 @@ function startWebSocketServer({ port, host, path, tunnelIdHeaderName }) {
|
|
|
41
43
|
|
|
42
44
|
const interval = setInterval(() => {
|
|
43
45
|
if (!ws.isAlive) {
|
|
44
|
-
logger.warn(
|
|
46
|
+
logger.warn(
|
|
47
|
+
`No pong received from client on tunnel [${tunnelId || 'unknown'}], terminating.`
|
|
48
|
+
);
|
|
45
49
|
return ws.terminate();
|
|
46
50
|
}
|
|
47
51
|
ws.isAlive = false;
|
|
@@ -51,7 +55,7 @@ function startWebSocketServer({ port, host, path, tunnelIdHeaderName }) {
|
|
|
51
55
|
}
|
|
52
56
|
}, PING_INTERVAL);
|
|
53
57
|
|
|
54
|
-
ws.on('message',
|
|
58
|
+
ws.on('message', chunk => {
|
|
55
59
|
logger.trace(`Received message chunk: ${chunk.length} bytes`);
|
|
56
60
|
buffer = Buffer.concat([buffer, chunk]);
|
|
57
61
|
|
|
@@ -67,15 +71,22 @@ function startWebSocketServer({ port, host, path, tunnelIdHeaderName }) {
|
|
|
67
71
|
const type = message.readUInt8(72);
|
|
68
72
|
const payload = message.slice(73);
|
|
69
73
|
|
|
70
|
-
logger.trace(
|
|
74
|
+
logger.trace(
|
|
75
|
+
`Parsed message - tunnelId: ${messageTunnelId}, uuid: ${uuid}, type: ${type}, payload length: ${payload.length}`
|
|
76
|
+
);
|
|
71
77
|
|
|
72
78
|
// Check for duplicate tunnelId on first message (when tunnelId is not yet set)
|
|
73
79
|
if (!tunnelId && messageTunnelId) {
|
|
74
80
|
const existingTunnel = state[portKey]?.websocketTunnels?.[messageTunnelId];
|
|
75
81
|
if (existingTunnel && existingTunnel.ws && existingTunnel.ws !== ws) {
|
|
76
82
|
// Check if the existing WebSocket is still open
|
|
77
|
-
if (
|
|
78
|
-
|
|
83
|
+
if (
|
|
84
|
+
existingTunnel.ws.readyState === WebSocket.OPEN ||
|
|
85
|
+
existingTunnel.ws.readyState === WebSocket.CONNECTING
|
|
86
|
+
) {
|
|
87
|
+
logger.error(
|
|
88
|
+
`Tunnel [${messageTunnelId}] already exists with an active connection. Rejecting new connection.`
|
|
89
|
+
);
|
|
79
90
|
|
|
80
91
|
// Assign tunnelId before closing so cleanup logs the correct value
|
|
81
92
|
tunnelId = messageTunnelId;
|
|
@@ -84,7 +95,9 @@ function startWebSocketServer({ port, host, path, tunnelIdHeaderName }) {
|
|
|
84
95
|
ws.close(1008, `Duplicate tunnelId: ${messageTunnelId}`);
|
|
85
96
|
return;
|
|
86
97
|
} else {
|
|
87
|
-
logger.info(
|
|
98
|
+
logger.info(
|
|
99
|
+
`Existing tunnel [${messageTunnelId}] has a closed connection. Allowing new connection.`
|
|
100
|
+
);
|
|
88
101
|
}
|
|
89
102
|
}
|
|
90
103
|
tunnelId = messageTunnelId;
|
|
@@ -104,7 +117,9 @@ function startWebSocketServer({ port, host, path, tunnelIdHeaderName }) {
|
|
|
104
117
|
delete state[portKey].websocketTunnels[tunnelId];
|
|
105
118
|
logger.debug(`Removed tunnel [${tunnelId}] from state`);
|
|
106
119
|
} else {
|
|
107
|
-
logger.debug(
|
|
120
|
+
logger.debug(
|
|
121
|
+
`Tunnel [${tunnelId}] not removed - this was a duplicate/rejected connection`
|
|
122
|
+
);
|
|
108
123
|
}
|
|
109
124
|
} else {
|
|
110
125
|
logger.debug(`No tunnelId assigned yet, nothing to remove from state`);
|
|
@@ -126,13 +141,13 @@ function startWebSocketServer({ port, host, path, tunnelIdHeaderName }) {
|
|
|
126
141
|
cleanup('close');
|
|
127
142
|
});
|
|
128
143
|
|
|
129
|
-
ws.on('error',
|
|
144
|
+
ws.on('error', err => {
|
|
130
145
|
logger.error(`WebSocket error on tunnel [${tunnelId || 'unknown'}]:`, err);
|
|
131
146
|
cleanup('error');
|
|
132
147
|
});
|
|
133
148
|
});
|
|
134
149
|
|
|
135
|
-
state[portKey].webSocketServer.on('error',
|
|
150
|
+
state[portKey].webSocketServer.on('error', err => {
|
|
136
151
|
logger.error('WebSocket server error:', err);
|
|
137
152
|
});
|
|
138
153
|
|
package/utils/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import cjsModule from './index.js';
|
|
2
|
-
export const { setLogLevel, getLogLevel, loadConfig } = cjsModule;
|
|
2
|
+
export const { setLogLevel, getLogLevel, loadConfig } = cjsModule;
|
package/utils/loadConfig.js
CHANGED
|
@@ -7,7 +7,9 @@ const TOML = require('@iarna/toml');
|
|
|
7
7
|
|
|
8
8
|
function loadConfig(customPath) {
|
|
9
9
|
const callerDir = require.main?.path || process.cwd();
|
|
10
|
-
const configPath = customPath
|
|
10
|
+
const configPath = customPath
|
|
11
|
+
? path.join(customPath, FILE_CONFIG_NAME)
|
|
12
|
+
: path.join(callerDir, FILE_CONFIG_NAME);
|
|
11
13
|
|
|
12
14
|
console.log({ configPath });
|
|
13
15
|
|
|
@@ -25,7 +27,9 @@ function loadConfig(customPath) {
|
|
|
25
27
|
logger.warn(`â ī¸ Failed to parse config.toml at ${configPath}: ${err.message}`);
|
|
26
28
|
}
|
|
27
29
|
} else {
|
|
28
|
-
logger.info(
|
|
30
|
+
logger.info(
|
|
31
|
+
`âšī¸ No config.toml found at: ${configPath}, falling back to environment variables.`
|
|
32
|
+
);
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
const envConfig = {
|
|
@@ -33,7 +37,9 @@ function loadConfig(customPath) {
|
|
|
33
37
|
wsUrl: process.env.WS_URL,
|
|
34
38
|
targetUrl: process.env.TARGET_URL,
|
|
35
39
|
tunnelEntryUrl: process.env.TUNNEL_ENTRY_URL,
|
|
36
|
-
tunnelEntryPort: process.env.TUNNEL_ENTRY_PORT
|
|
40
|
+
tunnelEntryPort: process.env.TUNNEL_ENTRY_PORT
|
|
41
|
+
? Number(process.env.TUNNEL_ENTRY_PORT)
|
|
42
|
+
: undefined,
|
|
37
43
|
headers: process.env.HEADERS,
|
|
38
44
|
allowInsicureCerts: process.env.ALLOW_INSICURE_CERTS === 'true',
|
|
39
45
|
logLevel: process.env.LOG_LEVEL || 'info',
|
package/utils/logger.js
CHANGED
|
@@ -34,12 +34,12 @@ const logger = winston.createLogger({
|
|
|
34
34
|
new winston.transports.Console({
|
|
35
35
|
format: winston.format.combine(
|
|
36
36
|
winston.format.timestamp(),
|
|
37
|
-
winston.format.printf(
|
|
37
|
+
winston.format.printf(info => {
|
|
38
38
|
const contextPrefix = logContext ? `${logContext} | ` : '';
|
|
39
39
|
// Get the raw level (before colorization)
|
|
40
40
|
const rawLevel = info[Symbol.for('level')] || info.level || 'info';
|
|
41
41
|
// Apply color to level and message separately
|
|
42
|
-
|
|
42
|
+
const colorizer = winston.format.colorize({ colors: customLevels.colors });
|
|
43
43
|
const coloredLevel = colorizer.colorize(rawLevel, rawLevel);
|
|
44
44
|
const coloredMessage = colorizer.colorize(rawLevel, `${contextPrefix}${info.message}`);
|
|
45
45
|
return `[${info.timestamp}] ${coloredLevel}: ${coloredMessage}`;
|