@remotelinker/reverse-ws-tunnel 1.0.4
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/CHANGELOG.md +54 -0
- package/README.md +406 -0
- package/client/index.js +34 -0
- package/client/proxyServer.js +74 -0
- package/client/tunnelClient.js +196 -0
- package/client/utils.js +24 -0
- package/cookbook.md +595 -0
- package/package.json +47 -0
- package/server/constants.js +7 -0
- package/server/index.js +8 -0
- package/server/messageHandler.js +79 -0
- package/server/state.js +1 -0
- package/server/tcpServer.js +137 -0
- package/server/websocketServer.js +142 -0
- package/utils/index.js +8 -0
- package/utils/loadConfig.js +54 -0
- package/utils/logger.js +136 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
const WebSocket = require('ws');
|
|
2
|
+
const { EventEmitter } = require('events');
|
|
3
|
+
const net = require('net');
|
|
4
|
+
const { v4: uuidv4 } = require('uuid');
|
|
5
|
+
const { buildMessageBuffer } = require('./utils');
|
|
6
|
+
const { logger } = require('../utils/logger');
|
|
7
|
+
const packageJson = require('../package.json');
|
|
8
|
+
|
|
9
|
+
const RECONNECT_INTERVAL = 5000;
|
|
10
|
+
const MESSAGE_TYPE_CONFIG = 0x01;
|
|
11
|
+
const MESSAGE_TYPE_DATA = 0x02;
|
|
12
|
+
const clients = {};
|
|
13
|
+
const PING_INTERVAL = 30 * 1000; //30s
|
|
14
|
+
const PONG_WAIT = 5 * 1000; //5s
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Starts the WebSocket tunnel client.
|
|
18
|
+
* @param {Object} config - Configuration for tunnel.
|
|
19
|
+
*/
|
|
20
|
+
function connectWebSocket(config) {
|
|
21
|
+
const { wsUrl, tunnelId, targetUrl, targetPort, tunnelEntryUrl, tunnelEntryPort, headers, environment, autoReconnect = true } = config;
|
|
22
|
+
|
|
23
|
+
const eventEmitter = new EventEmitter();
|
|
24
|
+
let ws;
|
|
25
|
+
let pingInterval;
|
|
26
|
+
let isClosed = false;
|
|
27
|
+
|
|
28
|
+
if (!tunnelId) {
|
|
29
|
+
throw new Error(`Missing mandatory tunnelId`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const connect = () => {
|
|
33
|
+
if (isClosed) return;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const headersParsed = headers || '{}';
|
|
37
|
+
logger.debug(`Parsed headers: ${JSON.stringify(headersParsed)}`);
|
|
38
|
+
logger.debug(`Try to connect to: ${wsUrl}`);
|
|
39
|
+
ws = new WebSocket(wsUrl, { headers: headersParsed });
|
|
40
|
+
logger.debug(`Connection: ${wsUrl}`);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
logger.error('Malformed headers:', error);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
ws.on('open', () => {
|
|
47
|
+
logger.info(`Connected to WebSocket server ${wsUrl}`);
|
|
48
|
+
logger.warn(`WS tunnel config sent: TARGET_PORT=${targetPort}, ENTRY_PORT=${tunnelEntryPort}`);
|
|
49
|
+
eventEmitter.emit('connected');
|
|
50
|
+
({ pingInterval } = heartBeat(ws));
|
|
51
|
+
|
|
52
|
+
const uuid = uuidv4();
|
|
53
|
+
const payload = {
|
|
54
|
+
TARGET_URL: targetUrl,
|
|
55
|
+
TARGET_PORT: targetPort,
|
|
56
|
+
TUNNEL_ENTRY_URL: tunnelEntryUrl,
|
|
57
|
+
TUNNEL_ENTRY_PORT: tunnelEntryPort,
|
|
58
|
+
environment,
|
|
59
|
+
agentVersion: packageJson.version,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const message = buildMessageBuffer(tunnelId, uuid, MESSAGE_TYPE_CONFIG, JSON.stringify(payload));
|
|
63
|
+
logger.debug(`Sending tunnel config [uuid=${uuid}]`);
|
|
64
|
+
ws.send(message);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
ws.on('message', (data) => {
|
|
68
|
+
const uuid = data.slice(0, 36).toString();
|
|
69
|
+
const type = data.readUInt8(36);
|
|
70
|
+
const payload = data.slice(37);
|
|
71
|
+
|
|
72
|
+
logger.trace(`Received WS message for uuid=${uuid}, type=${type}, length=${payload.length}`);
|
|
73
|
+
|
|
74
|
+
if (type === MESSAGE_TYPE_DATA) {
|
|
75
|
+
if (payload.toString() === 'CLOSE') {
|
|
76
|
+
logger.debug(`Received CLOSE for uuid=${uuid}`);
|
|
77
|
+
if (clients[uuid]) {
|
|
78
|
+
clients[uuid].end();
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const client = clients[uuid] || createTcpClient(targetUrl, targetPort, ws, tunnelId, uuid);
|
|
84
|
+
|
|
85
|
+
if (!client.write(payload)) {
|
|
86
|
+
logger.debug(`Backpressure on TCP socket for uuid=${uuid}`);
|
|
87
|
+
client.once('drain', () => {
|
|
88
|
+
logger.info(`TCP socket drained for uuid=${uuid}`);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
ws.on('close', () => {
|
|
95
|
+
logger.warn('WebSocket connection closed. Cleaning up clients.');
|
|
96
|
+
eventEmitter.emit('disconnected');
|
|
97
|
+
clearInterval(pingInterval);
|
|
98
|
+
|
|
99
|
+
for (const uuid in clients) {
|
|
100
|
+
logger.debug(`Closing TCP connection for uuid=${uuid}`);
|
|
101
|
+
clients[uuid].end();
|
|
102
|
+
clients[uuid].destroy();
|
|
103
|
+
delete clients[uuid];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!isClosed && autoReconnect) {
|
|
107
|
+
logger.info(`Reconnecting in ${RECONNECT_INTERVAL / 1000}s...`);
|
|
108
|
+
setTimeout(connect, RECONNECT_INTERVAL);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
ws.on('error', (err) => {
|
|
113
|
+
logger.error('WebSocket error:', err);
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
connect();
|
|
118
|
+
|
|
119
|
+
eventEmitter.close = () => {
|
|
120
|
+
isClosed = true;
|
|
121
|
+
if (ws) {
|
|
122
|
+
ws.terminate();
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
return eventEmitter;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Sets up heartbeat (ping/pong) mechanism.
|
|
131
|
+
*/
|
|
132
|
+
function heartBeat(ws) {
|
|
133
|
+
const pingInterval = setInterval(() => {
|
|
134
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
135
|
+
ws.ping();
|
|
136
|
+
logger.trace('Sent WebSocket ping');
|
|
137
|
+
|
|
138
|
+
const pongTimeout = setTimeout(() => {
|
|
139
|
+
logger.warn('No pong received. Terminating connection.');
|
|
140
|
+
ws.terminate();
|
|
141
|
+
}, PONG_WAIT);
|
|
142
|
+
|
|
143
|
+
ws.once('pong', () => {
|
|
144
|
+
logger.trace('Received WebSocket pong');
|
|
145
|
+
clearTimeout(pongTimeout);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}, PING_INTERVAL);
|
|
149
|
+
|
|
150
|
+
return { pingInterval };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Creates a TCP connection to the target service.
|
|
155
|
+
*/
|
|
156
|
+
function createTcpClient(targetUrl, targetPort, ws, tunnelId, uuid) {
|
|
157
|
+
const hostname = new URL(targetUrl).hostname;
|
|
158
|
+
logger.debug(`Creating TCP connection to ${hostname}:${targetPort} for uuid=${uuid}`);
|
|
159
|
+
|
|
160
|
+
const client = net.createConnection(targetPort, hostname);
|
|
161
|
+
clients[uuid] = client;
|
|
162
|
+
|
|
163
|
+
client.on('connect', () => {
|
|
164
|
+
logger.info(`TCP connection established for uuid=${uuid}`);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
client.on('data', (data) => {
|
|
168
|
+
logger.trace(`TCP data received for uuid=${uuid}, length=${data.length}`);
|
|
169
|
+
const message = buildMessageBuffer(tunnelId, uuid, MESSAGE_TYPE_DATA, data);
|
|
170
|
+
ws.send(message);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
client.on('error', (err) => {
|
|
174
|
+
logger.error(`TCP error for uuid=${uuid}:`, err);
|
|
175
|
+
client.destroy();
|
|
176
|
+
delete clients[uuid];
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
client.on('end', () => {
|
|
180
|
+
logger.info(`TCP connection ended for uuid=${uuid}`);
|
|
181
|
+
delete clients[uuid];
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
return client;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function resetClients() { // for testing
|
|
188
|
+
for (const key in clients) {
|
|
189
|
+
delete clients[key];
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = {
|
|
194
|
+
connectWebSocket,
|
|
195
|
+
resetClients, // for testing
|
|
196
|
+
};
|
package/client/utils.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds a binary message buffer.
|
|
3
|
+
* @param {string} tunnelId
|
|
4
|
+
* @param {string} uuid
|
|
5
|
+
* @param {number} type
|
|
6
|
+
* @param {Buffer|string} payload
|
|
7
|
+
* @returns {Buffer}
|
|
8
|
+
*/
|
|
9
|
+
function buildMessageBuffer(tunnelId, uuid, type, payload) {
|
|
10
|
+
const tunnelBuffer = Buffer.from(tunnelId);
|
|
11
|
+
const uuidBuffer = Buffer.from(uuid);
|
|
12
|
+
const typeBuffer = Buffer.from([type]);
|
|
13
|
+
const payloadBuffer = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, 'utf8');
|
|
14
|
+
|
|
15
|
+
const totalLength = tunnelBuffer.length + uuidBuffer.length + typeBuffer.length + payloadBuffer.length;
|
|
16
|
+
const lengthBuffer = Buffer.alloc(4);
|
|
17
|
+
lengthBuffer.writeUInt32BE(totalLength);
|
|
18
|
+
|
|
19
|
+
return Buffer.concat([lengthBuffer, tunnelBuffer, uuidBuffer, typeBuffer, payloadBuffer]);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = {
|
|
23
|
+
buildMessageBuffer,
|
|
24
|
+
};
|