@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 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({ targetUrl, allowInsicureCerts, wsUrl, tunnelId, tunnelEntryUrl, tunnelEntryPort, headers, environment, autoReconnect }) {
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;
@@ -24,23 +24,34 @@ function startHttpProxyServer(targetUrl, allowInsecureCerts = false) {
24
24
  return res.end('Missing TARGET_URL');
25
25
  }
26
26
 
27
- proxy.web(req, res, { target: targetUrl, changeOrigin: true, secure: !allowInsecureCerts }, (err) => {
28
- logger.error('Proxy web error:', err);
29
- if (!res.headersSent) {
30
- res.writeHead(502);
31
- res.end('Bad gateway');
32
- } else {
33
- res.end();
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(req, socket, head, { target: targetUrl, changeOrigin: false, secure: !allowInsecureCerts }, (err) => {
41
- logger.error('Proxy WS upgrade error:', err);
42
- socket.end();
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) => {
@@ -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 the WebSocket tunnel client.
21
+ * Starts WebSocket tunnel client.
23
22
  * @param {Object} config - Configuration for tunnel.
24
23
  */
25
24
  function connectWebSocket(config) {
26
- const { wsUrl, tunnelId, targetUrl, targetPort, tunnelEntryUrl, tunnelEntryPort, headers, environment, autoReconnect = true } = config;
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
- const headersParsed = headers || '{}';
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('Malformed headers:', 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(`WS tunnel config sent: TARGET_PORT=${targetPort}, ENTRY_PORT=${tunnelEntryPort}`);
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, { pingSeq: () => pingSeq, incPingSeq: () => pingSeq++ });
64
- healthMonitor = startHealthMonitor(ws, tunnelId, { lastPongTs: () => lastPongTs, setLastPongTs: (ts) => lastPongTs = ts });
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(tunnelId, uuid, MESSAGE_TYPE_CONFIG, JSON.stringify(payload));
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', (chunk) => {
84
- logger.trace(`Received message chunk: ${chunk.length} bytes`);
85
- messageBuffer = Buffer.concat([messageBuffer, chunk]);
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) break;
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
- logger.trace(`Received WS message for uuid=${uuid}, type=${type}, length=${payload.length}`);
100
-
101
- if (type === MESSAGE_TYPE_DATA) {
102
- if (payload.toString() === 'CLOSE') {
103
- logger.debug(`Received CLOSE for uuid=${uuid}`);
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
- const client = clients[uuid] || createTcpClient(targetUrl, targetPort, ws, tunnelId, uuid);
111
-
112
- } else if (type === MESSAGE_TYPE_APP_PONG) {
113
- try {
114
- const pongData = JSON.parse(payload.toString());
115
- // Accetta solo pong con seq >= pingSeq - 10 (finestra di 10 ping)
116
- if (pongData.seq >= (pingSeq - 10)) {
117
- lastPongTs = Date.now();
118
- logger.trace(`App pong received: seq=${pongData.seq}`);
119
- } else {
120
- logger.debug(`Ignoring old pong: seq=${pongData.seq}`);
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
- } catch (err) {
123
- logger.error(`Invalid app pong format: ${err.message}`);
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 = RECONNECT_BACKOFF[reconnectAttempt] || RECONNECT_BACKOFF[RECONNECT_BACKOFF.length - 1];
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', (err) => {
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 the target service.
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', (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', (err) => {
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() { // for testing
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 = tunnelBuffer.length + uuidBuffer.length + typeBuffer.length + payloadBuffer.length;
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
@@ -5,5 +5,5 @@ const utils = require('./utils');
5
5
  module.exports = {
6
6
  ...server,
7
7
  ...client,
8
- ...utils
9
- };
8
+ ...utils,
9
+ };
package/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  export * from './server/index.mjs';
2
2
  export * from './client/index.mjs';
3
- export * from './utils/index.mjs';
3
+ export * from './utils/index.mjs';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remotelinker/reverse-ws-tunnel",
3
- "version": "1.0.8",
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;
@@ -1,5 +1,10 @@
1
1
  const state = require('./state');
2
- const { MESSAGE_TYPE_CONFIG, MESSAGE_TYPE_DATA, MESSAGE_TYPE_APP_PING, MESSAGE_TYPE_APP_PONG } = require('./constants');
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(`Starting new TCP server on port ${TUNNEL_ENTRY_PORT} for tunnelId=${tunnelId}`);
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(`Failed to process MESSAGE_TYPE_CONFIG for tunnelId=${tunnelId}: ${error.message}`);
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);
@@ -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((socket) => {
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] = (info) => {
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(`Sending initial headers (${rawHeaders.length} bytes) to tunnel [${currentTunnelId}]`);
63
- tunnel.ws.send(Buffer.concat([uuidBuffer, Buffer.from([MESSAGE_TYPE_DATA]), Buffer.from(rawHeaders)]));
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
- tunnel.ws.send(Buffer.concat([uuidBuffer, Buffer.from([MESSAGE_TYPE_DATA]), body]));
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', (chunk) => {
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(`Forwarding WebSocket TCP data (${chunk.length} bytes) for tunnel [${currentTunnelId}]`);
94
- tunnel.ws.send(Buffer.concat([uuidBuffer, Buffer.from([MESSAGE_TYPE_DATA]), chunk]));
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
- tunnel.ws.send(Buffer.concat([uuidBuffer, Buffer.from([MESSAGE_TYPE_DATA]), Buffer.from('CLOSE')]));
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 = delete state[wsPortKey]?.websocketTunnels?.[currentTunnelId]?.tcpConnections?.[uuid];
116
- logger.debug(`TCP socket closed [${uuid}] for tunnel [${currentTunnelId}], connection ${deleted ? 'removed' : 'not found'}`);
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', (err) => {
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', (err) => {
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(`WebSocket server listening on port ${port}${host ? ` (host: ${host})` : ''}${path ? `, path: ${path}` : ''}`);
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(`No pong received from client on tunnel [${tunnelId || 'unknown'}], terminating.`);
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', (chunk) => {
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(`Parsed message - tunnelId: ${messageTunnelId}, uuid: ${uuid}, type: ${type}, payload length: ${payload.length}`);
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 (existingTunnel.ws.readyState === WebSocket.OPEN || existingTunnel.ws.readyState === WebSocket.CONNECTING) {
78
- logger.error(`Tunnel [${messageTunnelId}] already exists with an active connection. Rejecting new connection.`);
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(`Existing tunnel [${messageTunnelId}] has a closed connection. Allowing new connection.`);
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(`Tunnel [${tunnelId}] not removed - this was a duplicate/rejected connection`);
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', (err) => {
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', (err) => {
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;
@@ -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 ? path.join(customPath, FILE_CONFIG_NAME) : path.join(callerDir, FILE_CONFIG_NAME);
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(`â„šī¸ No config.toml found at: ${configPath}, falling back to environment variables.`);
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 ? Number(process.env.TUNNEL_ENTRY_PORT) : undefined,
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((info) => {
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
- const colorizer = winston.format.colorize({ colors: customLevels.colors });
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}`;