@remotelinker/reverse-ws-tunnel 1.0.9 → 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 +11 -1
- package/client/index.js +11 -1
- package/client/index.mjs +1 -1
- package/client/proxyServer.js +23 -12
- package/client/tunnelClient.js +90 -26
- 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 +16 -9
- 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,7 +30,17 @@ Reverse WebSocket Tunnel is a library that enables you to expose local services
|
|
|
30
30
|
|
|
31
31
|
---
|
|
32
32
|
|
|
33
|
-
## ✨ v1.0.
|
|
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
|
|
34
44
|
|
|
35
45
|
### 🐛 Bug Fixes
|
|
36
46
|
- **Message Format Standardization**: Fixed inconsistent message formats between server components
|
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;
|
|
@@ -23,7 +22,17 @@ const RECONNECT_BACKOFF = [1000, 2000, 5000, 10000, 30000]; // Backoff progressi
|
|
|
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,33 +114,50 @@ 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',
|
|
128
|
+
|
|
129
|
+
ws.on('message', data => {
|
|
84
130
|
logger.trace(`Received message chunk: ${data.length} bytes`);
|
|
85
131
|
messageBuffer = Buffer.concat([messageBuffer, data]);
|
|
86
132
|
|
|
87
133
|
while (messageBuffer.length >= 4) {
|
|
88
134
|
const length = messageBuffer.readUInt32BE(0);
|
|
89
135
|
if (messageBuffer.length < 4 + length) {
|
|
90
|
-
logger.trace(
|
|
136
|
+
logger.trace(
|
|
137
|
+
`Waiting for more data: need ${4 + length} bytes, have ${messageBuffer.length}`
|
|
138
|
+
);
|
|
91
139
|
break;
|
|
92
140
|
}
|
|
93
141
|
|
|
94
142
|
const message = messageBuffer.slice(4, 4 + length);
|
|
95
143
|
messageBuffer = messageBuffer.slice(4 + length);
|
|
96
144
|
|
|
97
|
-
const messageTunnelId = message.slice(0, 36).toString();
|
|
145
|
+
const messageTunnelId = message.slice(0, 36).toString().trim();
|
|
98
146
|
const uuid = message.slice(36, 72).toString();
|
|
99
147
|
const type = message.readUInt8(72);
|
|
100
148
|
const payload = message.slice(73);
|
|
101
149
|
|
|
102
|
-
|
|
150
|
+
// Validate tunnelId matches expected tunnel
|
|
151
|
+
if (messageTunnelId !== tunnelId) {
|
|
152
|
+
logger.warn(
|
|
153
|
+
`Received message for wrong tunnel: ${messageTunnelId} (expected: ${tunnelId})`
|
|
154
|
+
);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
logger.trace(
|
|
159
|
+
`Received WS message for uuid=${uuid}, type=${type}, length=${payload.length}`
|
|
160
|
+
);
|
|
103
161
|
|
|
104
162
|
if (type === MESSAGE_TYPE_DATA) {
|
|
105
163
|
if (payload.toString() === 'CLOSE') {
|
|
@@ -110,7 +168,8 @@ function connectWebSocket(config) {
|
|
|
110
168
|
return;
|
|
111
169
|
}
|
|
112
170
|
|
|
113
|
-
const client =
|
|
171
|
+
const client =
|
|
172
|
+
clients[uuid] || createTcpClient(targetUrl, targetPort, ws, tunnelId, uuid);
|
|
114
173
|
|
|
115
174
|
if (!client.write(payload)) {
|
|
116
175
|
logger.debug(`Backpressure on TCP socket for uuid=${uuid}`);
|
|
@@ -119,13 +178,13 @@ function connectWebSocket(config) {
|
|
|
119
178
|
});
|
|
120
179
|
}
|
|
121
180
|
return;
|
|
122
|
-
|
|
123
181
|
} else if (type === MESSAGE_TYPE_APP_PONG) {
|
|
124
182
|
try {
|
|
125
183
|
const pongData = JSON.parse(payload.toString());
|
|
126
184
|
// Accetta solo pong con seq >= pingSeq - 10 (finestra di 10 ping)
|
|
127
|
-
if (pongData.seq >= (
|
|
128
|
-
lastPongTs
|
|
185
|
+
if (pongData.seq >= pingStateCallbacks.pingSeq() - 10) {
|
|
186
|
+
// Aggiorna lastPongTs usando il callback
|
|
187
|
+
pingStateCallbacks.setLastPongTs(Date.now());
|
|
129
188
|
logger.trace(`App pong received: seq=${pongData.seq}`);
|
|
130
189
|
} else {
|
|
131
190
|
logger.debug(`Ignoring old pong: seq=${pongData.seq}`);
|
|
@@ -152,8 +211,12 @@ function connectWebSocket(config) {
|
|
|
152
211
|
delete clients[uuid];
|
|
153
212
|
}
|
|
154
213
|
|
|
214
|
+
// Reset message buffer on close for proper reconnection
|
|
215
|
+
messageBuffer = Buffer.alloc(0);
|
|
216
|
+
|
|
155
217
|
if (!isClosed && autoReconnect) {
|
|
156
|
-
const delay =
|
|
218
|
+
const delay =
|
|
219
|
+
RECONNECT_BACKOFF[reconnectAttempt] || RECONNECT_BACKOFF[RECONNECT_BACKOFF.length - 1];
|
|
157
220
|
logger.info(`Reconnecting in ${delay / 1000}s (attempt ${reconnectAttempt + 1})`);
|
|
158
221
|
setTimeout(() => {
|
|
159
222
|
reconnectAttempt = Math.min(reconnectAttempt + 1, RECONNECT_BACKOFF.length);
|
|
@@ -162,7 +225,7 @@ function connectWebSocket(config) {
|
|
|
162
225
|
}
|
|
163
226
|
});
|
|
164
227
|
|
|
165
|
-
ws.on('error',
|
|
228
|
+
ws.on('error', err => {
|
|
166
229
|
logger.error('WebSocket error:', err);
|
|
167
230
|
});
|
|
168
231
|
};
|
|
@@ -217,13 +280,13 @@ function createTcpClient(targetUrl, targetPort, ws, tunnelId, uuid) {
|
|
|
217
280
|
logger.info(`TCP connection established for uuid=${uuid}`);
|
|
218
281
|
});
|
|
219
282
|
|
|
220
|
-
client.on('data',
|
|
283
|
+
client.on('data', data => {
|
|
221
284
|
logger.trace(`TCP data received for uuid=${uuid}, length=${data.length}`);
|
|
222
285
|
const message = buildMessageBuffer(tunnelId, uuid, MESSAGE_TYPE_DATA, data);
|
|
223
286
|
ws.send(message);
|
|
224
287
|
});
|
|
225
288
|
|
|
226
|
-
client.on('error',
|
|
289
|
+
client.on('error', err => {
|
|
227
290
|
logger.error(`TCP error for uuid=${uuid}:`, err);
|
|
228
291
|
client.destroy();
|
|
229
292
|
delete clients[uuid];
|
|
@@ -248,7 +311,7 @@ function startAppHeartbeat(ws, tunnelId, pingState) {
|
|
|
248
311
|
const pingData = JSON.stringify({
|
|
249
312
|
type: 'ping',
|
|
250
313
|
seq: currentPingSeq,
|
|
251
|
-
ts: Date.now()
|
|
314
|
+
ts: Date.now(),
|
|
252
315
|
});
|
|
253
316
|
|
|
254
317
|
const message = buildMessageBuffer(tunnelId, uuidv4(), MESSAGE_TYPE_APP_PING, pingData);
|
|
@@ -273,7 +336,8 @@ function startHealthMonitor(ws, tunnelId, pongState) {
|
|
|
273
336
|
}, 5000); // Check every 5 seconds
|
|
274
337
|
}
|
|
275
338
|
|
|
276
|
-
function resetClients() {
|
|
339
|
+
function resetClients() {
|
|
340
|
+
// for testing
|
|
277
341
|
for (const key in clients) {
|
|
278
342
|
delete clients[key];
|
|
279
343
|
}
|
|
@@ -282,4 +346,4 @@ function resetClients() { // for testing
|
|
|
282
346
|
module.exports = {
|
|
283
347
|
connectWebSocket,
|
|
284
348
|
resetClients, // for testing
|
|
285
|
-
};
|
|
349
|
+
};
|
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
|
@@ -11,7 +11,7 @@ function startTCPServer(port, tunnelIdHeaderName, websocketPort) {
|
|
|
11
11
|
const wsPortKey = String(websocketPort);
|
|
12
12
|
const tcpPortKey = String(port);
|
|
13
13
|
|
|
14
|
-
const server = net.createServer(
|
|
14
|
+
const server = net.createServer(socket => {
|
|
15
15
|
const uuid = uuidv4();
|
|
16
16
|
const uuidBuffer = Buffer.from(uuid);
|
|
17
17
|
let currentTunnelId = null;
|
|
@@ -22,7 +22,7 @@ function startTCPServer(port, tunnelIdHeaderName, websocketPort) {
|
|
|
22
22
|
function createParser() {
|
|
23
23
|
const parser = new HTTPParser(HTTPParser.REQUEST);
|
|
24
24
|
|
|
25
|
-
parser[HTTPParser.kOnHeadersComplete] =
|
|
25
|
+
parser[HTTPParser.kOnHeadersComplete] = info => {
|
|
26
26
|
const headers = info.headers.reduce((acc, val, i, arr) => {
|
|
27
27
|
if (i % 2 === 0) acc[val.toLowerCase()] = arr[i + 1];
|
|
28
28
|
return acc;
|
|
@@ -60,7 +60,9 @@ function startTCPServer(port, tunnelIdHeaderName, websocketPort) {
|
|
|
60
60
|
|
|
61
61
|
isWebSocket = headers['upgrade']?.toLowerCase() === 'websocket';
|
|
62
62
|
|
|
63
|
-
logger.trace(
|
|
63
|
+
logger.trace(
|
|
64
|
+
`Sending initial headers (${rawHeaders.length} bytes) to tunnel [${currentTunnelId}]`
|
|
65
|
+
);
|
|
64
66
|
const message = buildMessageBuffer(currentTunnelId, uuid, MESSAGE_TYPE_DATA, rawHeaders);
|
|
65
67
|
tunnel.ws.send(message);
|
|
66
68
|
|
|
@@ -89,11 +91,13 @@ function startTCPServer(port, tunnelIdHeaderName, websocketPort) {
|
|
|
89
91
|
|
|
90
92
|
let currentParser = createParser();
|
|
91
93
|
|
|
92
|
-
socket.on('data',
|
|
94
|
+
socket.on('data', chunk => {
|
|
93
95
|
const tunnel = state[wsPortKey]?.websocketTunnels?.[currentTunnelId];
|
|
94
96
|
if (isWebSocket) {
|
|
95
97
|
if (tunnel?.ws) {
|
|
96
|
-
logger.trace(
|
|
98
|
+
logger.trace(
|
|
99
|
+
`Forwarding WebSocket TCP data (${chunk.length} bytes) for tunnel [${currentTunnelId}]`
|
|
100
|
+
);
|
|
97
101
|
const message = buildMessageBuffer(currentTunnelId, uuid, MESSAGE_TYPE_DATA, chunk);
|
|
98
102
|
tunnel.ws.send(message);
|
|
99
103
|
}
|
|
@@ -117,11 +121,14 @@ function startTCPServer(port, tunnelIdHeaderName, websocketPort) {
|
|
|
117
121
|
});
|
|
118
122
|
|
|
119
123
|
socket.on('close', () => {
|
|
120
|
-
const deleted =
|
|
121
|
-
|
|
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
|
+
);
|
|
122
129
|
});
|
|
123
130
|
|
|
124
|
-
socket.on('error',
|
|
131
|
+
socket.on('error', err => {
|
|
125
132
|
logger.error(`Socket error on tunnel [${currentTunnelId}], uuid [${uuid}]:`, err);
|
|
126
133
|
delete state[wsPortKey]?.websocketTunnels?.[currentTunnelId]?.tcpConnections?.[uuid];
|
|
127
134
|
});
|
|
@@ -134,7 +141,7 @@ function startTCPServer(port, tunnelIdHeaderName, websocketPort) {
|
|
|
134
141
|
logger.info(`TCP server listening on port ${port} for websocketPort ${websocketPort}`);
|
|
135
142
|
});
|
|
136
143
|
|
|
137
|
-
server.on('error',
|
|
144
|
+
server.on('error', err => {
|
|
138
145
|
logger.error(`TCP server error on port ${port}:`, err);
|
|
139
146
|
});
|
|
140
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}`;
|