@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 +11 -0
- package/docker-compose.yml +35 -0
- package/nodes/wsTunnel.html +236 -0
- package/nodes/wsTunnel.js +501 -0
- package/package.json +40 -0
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
|
+
}
|