@yousolution/node-red-contrib-you-tunnel-websocket 0.0.1

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 ADDED
@@ -0,0 +1,11 @@
1
+ # @yousolution/node-red-contrib-you-tunnel-websocket
2
+
3
+ A Node-RED node to create a tunnel websocket
4
+
5
+ ## Install
6
+
7
+ Run the following command in the root directory of your Node-RED install
8
+
9
+ npm install @yousolution/node-red-contrib-you-tunnel-websocket
10
+
11
+ See [license](https://github.com/yousolution-cloud/node-red-contrib-you-tunnel-websocket/blob/master/LICENSE) (Apache License Version 2.0).
@@ -0,0 +1,35 @@
1
+ version: '3.7'
2
+
3
+ services:
4
+ nodered:
5
+ image: nodered/node-red:3.1.9-18
6
+ # network_mode: host
7
+ extra_hosts:
8
+ - 'api.yousolution.local:192.168.178.100'
9
+ volumes:
10
+ - ./data:/data
11
+ ports:
12
+ - '1880:1880'
13
+ # - '4443:4443'
14
+ deploy:
15
+ resources:
16
+ limits:
17
+ memory: 800M
18
+ environment:
19
+ NODE_ENV: 'dev'
20
+ # VIRTUAL_HOST: websocket.yousolution.local
21
+ # VIRTUAL_PORT: '4443'
22
+
23
+ networks:
24
+ ys_tools_net:
25
+ aliases:
26
+ - nodered_659eafe98b4d8400110381f1
27
+ # ys_ingress_net:
28
+ # aliases:
29
+ # - nodered_659eafe98b4d8400110381f1
30
+
31
+ networks:
32
+ ys_tools_net:
33
+ external: true
34
+ # ys_ingress_net:
35
+ # external: true
@@ -0,0 +1,236 @@
1
+ <!--
2
+ Copyright 2023 Andrea Trentin.
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+ http://www.apache.org/licenses/LICENSE-2.0
7
+ Unless required by applicable law or agreed to in writing, software
8
+ distributed under the License is distributed on an "AS IS" BASIS,
9
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+ See the License for the specific language governing permissions and
11
+ limitations under the License.
12
+ -->
13
+
14
+ <script type="text/x-red" data-template-name="wstunnel">
15
+ <div class="form-row">
16
+ <label for="node-config-input-name"><i class="fa fa-bookmark"></i> name</label>
17
+ <input type="text" id="node-config-input-name" placeholder="tunnel name" />
18
+ </div>
19
+ <div class="form-row">
20
+ <label for="node-config-input-host"><i class="fa fa-bookmark"></i> Host</label>
21
+ <input type="text" id="node-config-input-host" placeholder="localhost" style="width: 40%;" />
22
+ <label for="node-config-input-port" style="margin-left: 10px; width: 35px; "> Port</label>
23
+ <input type="text" id="node-config-input-port" placeholder="22" style="width:45px">
24
+ </div>
25
+ <div class="form-row">
26
+ <label for="node-config-input-srcAddr"><i class="fa fa-gear"></i> Source Address</label>
27
+ <input type="text" id="node-config-input-srcAddr" placeholder="localhost">
28
+ </div>
29
+ <div class="form-row">
30
+ <label for="node-config-input-srcPort"><i class="fa fa-gear"></i> Source Port</label>
31
+ <input type="text" id="node-config-input-srcPort">
32
+ </div>
33
+ <div class="form-row">
34
+ <label for="node-config-input-dstAddr"><i class="fa fa-gear"></i> Destination Address</label>
35
+ <input type="text" id="node-config-input-dstAddr" placeholder="localhost">
36
+ </div>
37
+ <div class="form-row">
38
+ <label for="node-config-input-dstPort"><i class="fa fa-gear"></i> Destination Port</label>
39
+ <input type="text" id="node-config-input-dstPort">
40
+ </div>
41
+ <div class="form-row">
42
+ <label for="node-config-input-timeout"><i class="fa fa-gear"></i> Timeout</label>
43
+ <input type="text" id="node-config-input-timeout">
44
+ </div>
45
+ </script>
46
+
47
+ <script type="text/javascript">
48
+ RED.nodes.registerType('wstunnel', {
49
+ category: 'config',
50
+ color: '#AAAA66',
51
+ defaults: {
52
+ name: { value: '', required: true },
53
+ host: { value: '' },
54
+ port: { value: '' },
55
+ srcAddr: { value: '' },
56
+ srcPort: { value: '', required: true },
57
+ dstAddr: { value: '' },
58
+ dstPort: { value: '' },
59
+ timeout: { value: '' },
60
+ },
61
+ label: function () {
62
+ return this.name;
63
+ },
64
+ oneditprepare: function () {},
65
+ });
66
+ </script>
67
+
68
+ <script type="text/x-red" data-template-name="wstunnel client">
69
+ <div class="form-row">
70
+ <label for="node-input-url"><i class="fa fa-bookmark"></i> Url</label>
71
+ <input type="text" id="node-input-url" placeholder="Url tunnel" />
72
+ </div>
73
+ </script>
74
+
75
+ <script type="text/x-red" data-help-name="wstunnel client">
76
+ <p>
77
+ This node establishes a WebSocket client connection to a remote WebSocket server,
78
+ enabling the creation of an HTTP tunnel over the WebSocket connection. It can be
79
+ used for creating secure connections for web services or routing HTTP traffic through
80
+ a WebSocket channel.
81
+ </p>
82
+
83
+ <h3>Parameters</h3>
84
+ <table class="node-help">
85
+ <tr>
86
+ <td><strong>Url</strong></td>
87
+ <td>The URL of the WebSocket server to connect to. This should be in the format `ws://` or `wss://` for secure WebSocket connections. For example: `wss://example.com/socket`.</td>
88
+ </tr>
89
+ </table>
90
+
91
+ <h3>Usage</h3>
92
+ <p>
93
+ Once configured, this node will initiate a WebSocket connection to the specified server URL.
94
+ After the connection is established, the node can be used to tunnel HTTP traffic to the remote server.
95
+ This can be useful for scenarios like securely forwarding HTTP requests over a WebSocket connection.
96
+ </p>
97
+
98
+ <h3>Example</h3>
99
+ <pre>
100
+ [
101
+ {
102
+ "id": "wstunnel-client-node",
103
+ "type": "wstunnel client",
104
+ "url": "wss://example.com/socket",
105
+ "x": 300,
106
+ "y": 200,
107
+ "wires": []
108
+ }
109
+ ]
110
+ </pre>
111
+
112
+ <h3>Notes</h3>
113
+ <ul>
114
+ <li>The node initiates a WebSocket connection when deployed and listens for incoming HTTP requests to tunnel.</li>
115
+ <li>If the connection is closed or fails, the node will attempt to reconnect based on internal reconnection logic.</li>
116
+ <li>Ensure the WebSocket server you're connecting to supports tunneling or is configured to handle this type of traffic.</li>
117
+ </ul>
118
+ </script>
119
+
120
+ <script type="text/javascript">
121
+ RED.nodes.registerType('wstunnel client', {
122
+ category: 'network',
123
+ color: '#a7c957',
124
+ defaults: {
125
+ url: { value: '' },
126
+ },
127
+ inputs: 1,
128
+ outputs: 0,
129
+ icon: 'font-awesome/fa-lock',
130
+ label: function () {
131
+ return this.name || 'tunnel Websocket client';
132
+ },
133
+ labelStyle: function () {
134
+ return this.name ? 'node_label_italic' : '';
135
+ },
136
+ });
137
+ </script>
138
+
139
+ <script type="text/x-red" data-template-name="wstunnel server">
140
+ <div class="form-row">
141
+ <label for="node-input-wstunnnel"><i class="fa fa-gears"></i> Cofigure Tunnel Websocket server</label>
142
+ <input type="text" id="node-input-wstunnel">
143
+ </div>
144
+ </script>
145
+
146
+ <script type="text/x-red" data-help-name="wstunnel server">
147
+ <p>
148
+ This node is used to configure and manage a WebSocket server tunnel. It allows the establishment
149
+ of WebSocket-based communication between a server and a client. You can define the source and destination
150
+ addresses and ports to tunnel the traffic through WebSocket connections.
151
+ </p>
152
+
153
+ <h3>Parameters</h3>
154
+ <table class="node-help">
155
+ <tr>
156
+ <td><strong>Name</strong></td>
157
+ <td>A unique name for the tunnel configuration. This will help you identify the tunnel in Node-RED.</td>
158
+ </tr>
159
+ <tr>
160
+ <td><strong>Host</strong></td>
161
+ <td>The host address to which the WebSocket server should connect. This is typically the address of your server hosting the WebSocket server.</td>
162
+ </tr>
163
+ <tr>
164
+ <td><strong>Port</strong></td>
165
+ <td>The port on which the WebSocket server will listen. This is the port used to accept WebSocket connections from clients.</td>
166
+ </tr>
167
+ <tr>
168
+ <td><strong>Source Address</strong></td>
169
+ <td>The source address (IP or hostname) for the WebSocket connection. This is typically set to the address of the service initiating the WebSocket connection.</td>
170
+ </tr>
171
+ <tr>
172
+ <td><strong>Source Port</strong></td>
173
+ <td>The source port that the WebSocket server will use. This is required to define the source of the communication.</td>
174
+ </tr>
175
+ <tr>
176
+ <td><strong>Destination Address</strong></td>
177
+ <td>The destination address where the WebSocket connection should forward the tunneled HTTP traffic. This is usually set to the IP or hostname of the remote service.</td>
178
+ </tr>
179
+ <tr>
180
+ <td><strong>Destination Port</strong></td>
181
+ <td>The destination port where the tunneled HTTP traffic will be sent. This corresponds to the port of the destination service.</td>
182
+ </tr>
183
+ <tr>
184
+ <td><strong>Timeout</strong></td>
185
+ <td>Timeout in milliseconds for the WebSocket connection. If the WebSocket connection does not establish within this time, the connection attempt will be aborted.</td>
186
+ </tr>
187
+ </table>
188
+
189
+ <h3>Usage</h3>
190
+ <p>
191
+ After configuring the tunnel server, the node will act as the WebSocket server. It listens for incoming
192
+ WebSocket connection requests and forwards any received HTTP traffic to the specified destination address and port.
193
+ </p>
194
+
195
+ <h3>Example</h3>
196
+ <pre>
197
+ [
198
+ {
199
+ "id": "wstunnel-server-node",
200
+ "type": "wstunnel server",
201
+ "wstunnel": "wstunnel-config",
202
+ "name": "My WebSocket Tunnel Server",
203
+ "x": 300,
204
+ "y": 200,
205
+ "wires": []
206
+ }
207
+ ]
208
+ </pre>
209
+
210
+ <h3>Notes</h3>
211
+ <ul>
212
+ <li>The server node requires a valid tunnel configuration to function. You can link a configuration node using the 'Configure Tunnel WebSocket server' field.</li>
213
+ <li>If the WebSocket server is not running, the node will attempt to reconnect based on the configured timeout.</li>
214
+ <li>Ensure the WebSocket server and client are compatible with the tunneling protocol.</li>
215
+ </ul>
216
+ </script>
217
+
218
+ <script type="text/javascript">
219
+ RED.nodes.registerType('wstunnel server', {
220
+ category: 'network',
221
+ color: '#a7c957',
222
+ defaults: {
223
+ wstunnel: { type: 'wstunnel', required: true },
224
+ name: { value: '' },
225
+ },
226
+ inputs: 0,
227
+ outputs: 0,
228
+ icon: 'font-awesome/fa-lock',
229
+ label: function () {
230
+ return this.name || 'tunnel Websocket server';
231
+ },
232
+ labelStyle: function () {
233
+ return this.name ? 'node_label_italic' : '';
234
+ },
235
+ });
236
+ </script>
@@ -0,0 +1,501 @@
1
+ /**
2
+ * Copyright 2015 Atsushi Kojo.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ **/
16
+ const WebSocket = require('ws');
17
+ const { Buffer } = require('buffer');
18
+ const net = require('net');
19
+ const { v4: uuidv4 } = require('uuid');
20
+ const tls = require('tls');
21
+
22
+ const PING_INTERVAL = 1000 * 30; // in seconds
23
+ const instances = {};
24
+ const HTTP_TIMEOUT = 1000 * 30; // in seconds
25
+ const RECONNECT_INTERVAL = 1000 * 5;
26
+
27
+ module.exports = function (RED) {
28
+ 'use strict';
29
+
30
+ function WSTunnelNode(n) {
31
+ RED.nodes.createNode(this, n);
32
+
33
+ const [dstProtocol, dstHost, dstPort] = getProtocolAndHostAndDefautlPort(n.dstAddr);
34
+
35
+ // console.log('{}{}{}{}{}{}{}{}');
36
+ // console.log(dstProtocol);
37
+ // console.log(dstHost);
38
+ // console.log(dstPort);
39
+ // console.log('{}{}{}{}{}{}{}{}');
40
+
41
+ this.options = {
42
+ name: n.name || 'websocket tunnel',
43
+ host: n.host || 'localhost',
44
+ port: n.port || 8083,
45
+
46
+ srcAddr: n.srcAddr || 'localhost',
47
+ srcPort: n.srcPort,
48
+ dstAddr: dstHost || 'localhost',
49
+ dstPort: n.dstPort || dstPort || 80,
50
+ timeout: n.timeout || HTTP_TIMEOUT,
51
+ };
52
+ }
53
+
54
+ RED.nodes.registerType('wstunnel', WSTunnelNode);
55
+
56
+ function WSTunnelClient(n) {
57
+ RED.nodes.createNode(this, n);
58
+ let node = this;
59
+ this.status({});
60
+
61
+ const websocketURL = n.url;
62
+
63
+ if (!websocketURL) {
64
+ node.status({ fill: 'red', shape: 'dot', text: 'The URL is invalid or not defined.' });
65
+ return;
66
+ }
67
+
68
+ function connectWebSocket() {
69
+ let options = undefined;
70
+ // Crea una connessione al server WebSocket
71
+ const ws = new WebSocket(websocketURL, options);
72
+
73
+ // Quando la connessione è aperta, invia un messaggio al server
74
+ ws.on('open', () => {
75
+ console.log('Connected to the WebSocket server');
76
+ node.status({ fill: 'green', shape: 'dot', text: 'WebSocket tunnel established' });
77
+ });
78
+
79
+ // Quando ricevi un messaggio dal server, stampalo
80
+ ws.on('message', async (data) => {
81
+ // console.log('Received from server: ', data);
82
+ // console.log('Received Base64 encoded HTTP RAW request:');
83
+
84
+ // Decodifica la stringa Base64
85
+ try {
86
+ // const request = JSON.parse(Buffer.from(data, 'base64').toString('utf-8'));
87
+ // const rawHttpRequest = Buffer.from(request.httpRequest, 'base64').toString('utf-8');
88
+ // console.log('Decoded HTTP RAW Request:', request.httpRequest);
89
+
90
+ const extractedUUID = data.slice(0, 36);
91
+ // console.log(extractedUUID);
92
+ const res = await incomingRequest(data.slice(36)); // tolgo dal buffer uuid
93
+ // console.log('======');
94
+ // console.log(res.toString('utf-8'));
95
+ // console.log('======');
96
+ ws.send(Buffer.concat([extractedUUID, res]));
97
+ } catch (error) {
98
+ console.log(error);
99
+ }
100
+ });
101
+
102
+ // Gestione degli errori di connessione
103
+ ws.on('error', (error) => {
104
+ node.status({ fill: 'red', shape: 'dot', text: 'Websocket error.' });
105
+ console.error('Error occurred: ', error);
106
+ });
107
+
108
+ // Quando la connessione è chiusa, stampa un messaggio
109
+ ws.on('close', () => {
110
+ console.log('Disconnected from the WebSocket server');
111
+ node.status({ fill: 'red', shape: 'ring', text: 'Disconnected from the WebSocket server' });
112
+ setTimeout(connectWebSocket, RECONNECT_INTERVAL);
113
+ });
114
+ }
115
+
116
+ connectWebSocket();
117
+
118
+ async function incomingRequest(rawHttpRequest) {
119
+ const { hostname, port } = extractHostAndPort(rawHttpRequest);
120
+
121
+ if (port == 443) {
122
+ return await tlsRequest(rawHttpRequest);
123
+ }
124
+
125
+ return await tcpRequest(rawHttpRequest);
126
+ }
127
+
128
+ function tlsRequest(rawHttpRequest) {
129
+ return new Promise((resolve, reject) => {
130
+ const { hostname, port } = extractHostAndPort(rawHttpRequest);
131
+
132
+ let responseData = Buffer.alloc(0); // Inizializza un buffer vuoto per accumulare i dati
133
+
134
+ // console.log(hostname);
135
+ // console.log(port);
136
+ // Creazione di un socket TCP
137
+ const client = tls.connect(port, hostname, { servername: hostname }, () => {
138
+ client.write(rawHttpRequest); // Invia la richiesta HTTP raw
139
+ });
140
+
141
+ // Ricevi i dati dalla risposta del server
142
+ client.on('data', (data) => {
143
+ // console.log('Received response from server:');
144
+ // console.log(data.toString()); // Converte i dati ricevuti in stringa e li stampa
145
+ // return resolve(data);
146
+ responseData = Buffer.concat([responseData, data]); // Accumula i dati ricevuti
147
+ });
148
+
149
+ // Quando la connessione si chiude
150
+ client.on('end', () => {
151
+ console.log('Connection closed');
152
+ resolve(responseData);
153
+ // client.destroy()
154
+ });
155
+
156
+ // Gestione degli errori
157
+ client.on('error', (err) => {
158
+ reject(err);
159
+ console.error('Error:', err);
160
+ });
161
+ });
162
+ }
163
+
164
+ function tcpRequest(rawHttpRequest) {
165
+ return new Promise((resolve, reject) => {
166
+ const { hostname, port } = extractHostAndPort(rawHttpRequest);
167
+
168
+ let responseData = Buffer.alloc(0); // Inizializza un buffer vuoto per accumulare i dati
169
+
170
+ // console.log(hostname);
171
+ // console.log(port);
172
+
173
+ // Creazione di un socket TCP
174
+ const client = net.createConnection(port, hostname, () => {
175
+ client.write(rawHttpRequest); // Invia la richiesta HTTP raw
176
+ });
177
+
178
+ // Ricevi i dati dalla risposta del server
179
+ client.on('data', (data) => {
180
+ responseData = Buffer.concat([responseData, data]); // Accumula i dati ricevuti
181
+ });
182
+
183
+ // Quando la connessione si chiude
184
+ client.on('end', () => {
185
+ console.log('Connection closed');
186
+ resolve(responseData);
187
+ });
188
+
189
+ // Gestione degli errori
190
+ client.on('error', (err) => {
191
+ reject(err);
192
+ console.error('Error:', err);
193
+ });
194
+ });
195
+ }
196
+
197
+ function extractHostAndPort(rawHttp) {
198
+ const hostPattern = /Host:\s*([^\r\n]+)/i; // Regex per trovare la linea Host
199
+ const rawHttpString = rawHttp.toString('utf-8');
200
+ const match = rawHttpString.match(hostPattern);
201
+
202
+ if (!match) {
203
+ throw new Error('Host non trovato nella stringa HTTP raw');
204
+ }
205
+
206
+ const host = match[1];
207
+ const [hostname, port = 443] = host.split(':'); // Porta di default 443 per HTTPS
208
+
209
+ return { hostname, port: parseInt(port, 10) };
210
+ }
211
+
212
+ this.url = n.url;
213
+ // this.operation = n.operation;
214
+ // this.filename = n.filename;
215
+ // this.fileExtension = n.fileExtension;
216
+ // this.workdir = n.workdir;
217
+ // this.tunnelConfig = RED.nodes.getNode(this.tunnel);
218
+
219
+ // const globalContext = this.context().global;
220
+
221
+ // if (!this.tunnelConfig) {
222
+ // this.error('missing tunnel Websocket configuration');
223
+ // return;
224
+ // }
225
+
226
+ // Hack for async function
227
+ // setTimeout(async () => {
228
+ // await connect(node);
229
+ // });
230
+
231
+ // node.on('input', async (msg, send, done) => {
232
+ // setTimeout(async () => {
233
+ // await connect(node);
234
+ // }, node.tunnelConfig.options.reconnectTimeout);
235
+ // });
236
+ }
237
+ RED.nodes.registerType('wstunnel client', WSTunnelClient);
238
+
239
+ function WSTunnelServer(n) {
240
+ instances[n.id] = instances[n.id] || {};
241
+ instances[n.id].server = instances[n.id].server || null;
242
+ instances[n.id].wss = instances[n.id].wss || null;
243
+ instances[n.id].websocketServer = instances[n.id].websocketServer || null;
244
+
245
+ RED.nodes.createNode(this, n);
246
+ this.tunnelConfig = RED.nodes.getNode(n.wstunnel);
247
+ let node = this;
248
+ const id = n.id;
249
+
250
+ node.status({ fill: 'red', shape: 'ring', text: 'disconnected' });
251
+
252
+ const port = node.tunnelConfig.options.port;
253
+ const srcAddr = node.tunnelConfig.options.srcAddr || 'localhost';
254
+ const srcPort = node.tunnelConfig.options.srcPort || 8083;
255
+ const dstAddr = node.tunnelConfig.options.dstAddr || 'localhost';
256
+ const dstPort = node.tunnelConfig.options.dstPort || 80;
257
+ const timeout = node.tunnelConfig.options.timeout || HTTP_TIMEOUT;
258
+ // const port = 4443;
259
+
260
+ // console.log('+++++++++ OPTIONS ++++++++');
261
+ // console.log(port);
262
+ // console.log(dstPort);
263
+ // console.log(dstAddr);
264
+ // console.log(timeout);
265
+ // console.log('+++++++++ OPTIONS ++++++++');
266
+ const options = {
267
+ id,
268
+ port,
269
+ srcAddr,
270
+ srcPort,
271
+ dstAddr,
272
+ dstPort,
273
+ timeout,
274
+ };
275
+
276
+ if (instances[options.id].server) {
277
+ instances[options.id].server.close(() => {
278
+ TCPServer(options);
279
+ });
280
+ } else {
281
+ TCPServer(options);
282
+ }
283
+
284
+ if (instances[n.id].wss) {
285
+ instances[n.id].wss.close(() => {
286
+ webSocketServer(node, options);
287
+ });
288
+ } else {
289
+ webSocketServer(node, options);
290
+ }
291
+ }
292
+
293
+ function requestFromServer(message, options) {
294
+ return new Promise((resolve, reject) => {
295
+ if (!instances[options.id].websocketServer) {
296
+ return reject(new WebsocketTunnelError('WebSocket connection not established.', 500));
297
+ }
298
+
299
+ const uuid = uuidv4();
300
+ const uuidBuffer = Buffer.from(uuid);
301
+ const resultBuffer = Buffer.concat([uuidBuffer, Buffer.from(message)]);
302
+
303
+ const timeoutId = setTimeout(() => {
304
+ clearTimeout(timeoutId);
305
+ reject('Timeout: No response from the client.');
306
+ }, options.timeout);
307
+
308
+ const onMessage = (response) => {
309
+ // console.log(response.toString());
310
+ const extractedUUID = response.slice(0, 36);
311
+ if (Buffer.compare(uuidBuffer, extractedUUID) === 0) {
312
+ resolve(response.slice(36));
313
+ instances[options.id].websocketServer.removeListener('message', onMessage);
314
+ clearTimeout(timeoutId);
315
+ }
316
+ };
317
+ instances[options.id].websocketServer.on('message', onMessage);
318
+
319
+ instances[options.id].websocketServer.send(resultBuffer);
320
+ });
321
+ }
322
+
323
+ function webSocketServer(node, options) {
324
+ try {
325
+ instances[node.id].wss = new WebSocket.Server({ port: options.port });
326
+ } catch (error) {
327
+ console.log(error);
328
+ }
329
+
330
+ instances[node.id].wss.on('listening', () => {
331
+ console.log(`WebSocket server is listening on port ${options.port}`);
332
+ });
333
+
334
+ instances[node.id].wss.on('connection', (ws) => {
335
+ if (instances[node.id].websocketServer) {
336
+ console.log('WebSocket tunnel is already active');
337
+ ws.close();
338
+ return;
339
+ }
340
+
341
+ console.log('WebSocket tunnel established');
342
+ node.status({ fill: 'green', shape: 'dot', text: 'WebSocket tunnel established' });
343
+ instances[node.id].websocketServer = ws;
344
+
345
+ // Invia un messaggio di benvenuto al client appena connesso
346
+ // ws.send('Ciao dal server WebSocket!');
347
+
348
+ const interval = setInterval(() => {
349
+ if (ws.readyState === WebSocket.OPEN) {
350
+ ws.ping(); // invia un ping al client
351
+ console.log('Ping inviato al client');
352
+ }
353
+ }, PING_INTERVAL);
354
+
355
+ // Quando si verifica un errore
356
+ ws.on('error', function error(err) {
357
+ instances[node.id].websocketServer = null;
358
+ console.error('WebSocket error:', err);
359
+ clearInterval(interval); // pulisci l'intervallo quando la connessione è chiusa
360
+ node.error(err, { payload: 'Open websockeet tunnel error', message: err.message });
361
+ node.status({ fill: 'red', shape: 'dot', text: 'Error' });
362
+ });
363
+
364
+ // Gestiamo i messaggi ricevuti dal client
365
+ // ws.on('message', (message) => {
366
+ // console.log(`Messaggio ricevuto dal client: ${message}`);
367
+ // // Rispondiamo al client
368
+ // // ws.send(`Hai detto: ${message}`);
369
+ // });
370
+
371
+ // Gestiamo la chiusura della connessione
372
+ ws.on('close', () => {
373
+ console.log('WebSocket tunnel closed');
374
+ instances[node.id].websocketServer = null;
375
+ node.status({ fill: 'red', shape: 'ring', text: 'disconnected' });
376
+ clearInterval(interval); // pulisci l'intervallo quando la connessione è chiusa
377
+ });
378
+
379
+ // Quando ricevi un pong dal client
380
+ ws.on('pong', () => {
381
+ console.log('Pong ricevuto dal client');
382
+ });
383
+ });
384
+
385
+ instances[node.id].wss.on('error', (error) => {
386
+ console.log(error);
387
+ });
388
+ }
389
+
390
+ function TCPServer(options) {
391
+ instances[options.id].server = net.createServer((socket) => {
392
+ // console.log('Client connesso');
393
+
394
+ // Variabile per memorizzare i dati grezzi della richiesta
395
+ let rawRequestData = '';
396
+
397
+ // Leggi i dati grezzi dal socket
398
+ socket.on('data', async (data) => {
399
+ rawRequestData += data.toString(); // Aggiungi i dati ricevuti alla variabile rawRequestData
400
+
401
+ // Puoi scegliere di processare la richiesta solo quando hai ricevuto tutto il contenuto, ad esempio quando la richiesta termina
402
+ if (rawRequestData.includes('\r\n\r\n')) {
403
+ // Quando i dati contengono l'header completo, puoi trattarli come una richiesta completa
404
+
405
+ // Sostituire il campo Host con il nuovo valore
406
+ rawRequestData = rawRequestData.replace(/Host:\s*([^\r\n]*)/, `Host: ${options.dstAddr}:${options.dstPort}`);
407
+ console.log('Richiesta HTTP raw ricevuta:');
408
+ console.log(rawRequestData);
409
+
410
+ try {
411
+ const response = await requestFromServer(rawRequestData, options);
412
+ // console.log('-----------');
413
+ // console.log(response);
414
+ // console.log('-----------');
415
+ socket.write(response);
416
+ // Rispondi al client (opzionale)
417
+ // socket.write('HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nResponse OK');
418
+ // Chiudi la connessione (opzionale)
419
+ } catch (error) {
420
+ console.log(error);
421
+ let errorRespose =
422
+ `HTTP/1.1 408 Request Timeout\r\n` +
423
+ `Content-Type: text/plain; charset=utf-8\r\n` +
424
+ // `Content-Length: 29\r\n` +
425
+ `Connection: close\r\n` +
426
+ `Date: ${new Date().toUTCString()}\r\n\r\n` + // usa toUTCString() invece di toGMTString
427
+ `Request Timeout: The server timed out waiting for the request.`;
428
+
429
+ if (error instanceof WebsocketTunnelError) {
430
+ errorRespose =
431
+ `HTTP/1.1 ${error.code} ${error.name}\r\n` +
432
+ `Content-Type: text/plain; charset=utf-8\r\n` +
433
+ // `Content-Length: 29\r\n` +
434
+ `Connection: close\r\n` +
435
+ `Date: ${new Date().toUTCString()}\r\n\r\n` + // usa toUTCString() invece di toGMTString
436
+ `${error.message}`;
437
+ }
438
+
439
+ socket.write(errorRespose); // Invia la risposta di timeout
440
+ }
441
+ socket.end();
442
+ }
443
+ });
444
+
445
+ socket.on('end', () => {
446
+ console.log('Client disconnected');
447
+ });
448
+
449
+ socket.on('error', (err) => {
450
+ console.error('Connection error:', err);
451
+ });
452
+ });
453
+
454
+ try {
455
+ instances[options.id].server.listen(options.srcPort, () => {
456
+ console.log(`HTTP server listening on port ${options.srcPort}`);
457
+ });
458
+ instances[options.id].server.on('error', (error) => {
459
+ console.log(error);
460
+ });
461
+ } catch (error) {
462
+ console.log(error);
463
+ }
464
+ }
465
+
466
+ function getProtocolAndHostAndDefautlPort(url) {
467
+ try {
468
+ const protocolPorts = {
469
+ http: 80,
470
+ https: 443,
471
+ ftp: 21,
472
+ ssh: 22,
473
+ };
474
+ // Add "http://" as a default protocol
475
+ if (!/^https?:\/\//i.test(url)) {
476
+ url = 'http://' + url;
477
+ }
478
+ const parsedUrl = new URL(url);
479
+ const protocol = parsedUrl.protocol.replace(':', '');
480
+ const host = parsedUrl.hostname;
481
+
482
+ const port = parsedUrl.port || protocolPorts[protocol] || null;
483
+
484
+ return [protocol, host, port];
485
+ } catch (error) {
486
+ console.error('Invalid URL:', error);
487
+ return null;
488
+ }
489
+ }
490
+
491
+ class WebsocketTunnelError extends Error {
492
+ constructor(message, code) {
493
+ super(message);
494
+ this.name = this.constructor.name;
495
+ this.code = code;
496
+ Error.captureStackTrace(this, this.constructor);
497
+ }
498
+ }
499
+
500
+ RED.nodes.registerType('wstunnel server', WSTunnelServer);
501
+ };
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@yousolution/node-red-contrib-you-tunnel-websocket",
3
+ "version": "0.0.1",
4
+ "description": "Module to create websockeet tunnel for NODE-RED",
5
+ "license": "MIT",
6
+ "scripts": {
7
+ "update": "npm pack && mv yousolution-node-red-contrib-you-tunnel-websocket-$npm_package_version.tgz ./data && cd data && rm -rf node_modules && npm i yousolution-node-red-contrib-you-tunnel-websocket-$npm_package_version.tgz && docker-compose restart",
8
+ "test": "mocha 'test/**/*.spec.js'",
9
+ "coverage": "nyc npm run test"
10
+ },
11
+ "keywords": [
12
+ "node-red",
13
+ "websocket",
14
+ "tunnel",
15
+ "integration",
16
+ "youSolution.Cloud"
17
+ ],
18
+ "author": "Andrea Trentin <andrea.trentin@yousolution.cloud>",
19
+ "node-red": {
20
+ "version": ">=2.0.0",
21
+ "nodes": {
22
+ "wsTunnel": "/nodes/wsTunnel.js"
23
+ }
24
+ },
25
+ "dependencies": {
26
+ "uuid": "^11.0.5"
27
+ },
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/yousolution-cloud/node-red-contrib-you-tunnel-websocket.git"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "devDependencies": {
36
+ "jest": "^29.5.0",
37
+ "node-red": "^3.0.2",
38
+ "node-red-node-test-helper": "^0.3.1"
39
+ }
40
+ }