diodejs 0.1.5 → 0.2.0
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 +78 -9
- package/bindPort.js +225 -50
- package/connection.js +158 -18
- package/examples/portForwardTest.js +3 -3
- package/package.json +1 -1
- package/publishPort.js +1 -1
package/README.md
CHANGED
|
@@ -15,6 +15,35 @@ If you want to enable debug logs, set environment variable DEBUG to true.
|
|
|
15
15
|
|
|
16
16
|
Can also use .env files
|
|
17
17
|
|
|
18
|
+
### Connection Settings
|
|
19
|
+
|
|
20
|
+
Connection retry behavior can be configured via environment variables:
|
|
21
|
+
|
|
22
|
+
| Environment Variable | Description | Default |
|
|
23
|
+
|----------------------|-------------|---------|
|
|
24
|
+
| DIODE_MAX_RETRIES | Maximum number of reconnection attempts | Infinity |
|
|
25
|
+
| DIODE_RETRY_DELAY | Initial delay between retries (ms) | 1000 |
|
|
26
|
+
| DIODE_MAX_RETRY_DELAY | Maximum delay between retries (ms) | 30000 |
|
|
27
|
+
| DIODE_AUTO_RECONNECT | Whether to automatically reconnect | true |
|
|
28
|
+
|
|
29
|
+
Example `.env` file:
|
|
30
|
+
```
|
|
31
|
+
DIODE_MAX_RETRIES=10
|
|
32
|
+
DIODE_RETRY_DELAY=2000
|
|
33
|
+
DIODE_MAX_RETRY_DELAY=20000
|
|
34
|
+
DIODE_AUTO_RECONNECT=true
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
These settings can also be configured programmatically:
|
|
38
|
+
```javascript
|
|
39
|
+
connection.setReconnectOptions({
|
|
40
|
+
maxRetries: 10,
|
|
41
|
+
retryDelay: 2000,
|
|
42
|
+
maxRetryDelay: 20000,
|
|
43
|
+
autoReconnect: true
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
18
47
|
### Test RPC
|
|
19
48
|
|
|
20
49
|
Here's a quick example to get you started with RPC functions using `DiodeRPC` Class
|
|
@@ -28,6 +57,26 @@ async function main() {
|
|
|
28
57
|
const keyLocation = './db/keys.json'; // Optional, defaults to './db/keys.json'
|
|
29
58
|
|
|
30
59
|
const connection = new DiodeConnection(host, port, keyLocation);
|
|
60
|
+
|
|
61
|
+
// Configure reconnection (optional - overrides environment variables)
|
|
62
|
+
connection.setReconnectOptions({
|
|
63
|
+
maxRetries: Infinity, // Unlimited reconnection attempts
|
|
64
|
+
retryDelay: 1000, // Initial delay of 1 second
|
|
65
|
+
maxRetryDelay: 30000, // Maximum delay of 30 seconds
|
|
66
|
+
autoReconnect: true // Automatically reconnect on disconnection
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Listen for reconnection events (optional)
|
|
70
|
+
connection.on('reconnecting', (info) => {
|
|
71
|
+
console.log(`Reconnecting... Attempt #${info.attempt} in ${info.delay}ms`);
|
|
72
|
+
});
|
|
73
|
+
connection.on('reconnected', () => {
|
|
74
|
+
console.log('Successfully reconnected!');
|
|
75
|
+
});
|
|
76
|
+
connection.on('reconnect_failed', () => {
|
|
77
|
+
console.log('Failed to reconnect after maximum attempts');
|
|
78
|
+
});
|
|
79
|
+
|
|
31
80
|
await connection.connect();
|
|
32
81
|
|
|
33
82
|
const rpc = new DiodeRPC(connection);
|
|
@@ -68,15 +117,23 @@ async function main() {
|
|
|
68
117
|
|
|
69
118
|
// Multiple or single port binding with configuration object
|
|
70
119
|
const portsConfig = {
|
|
71
|
-
3002: {
|
|
72
|
-
|
|
120
|
+
3002: {
|
|
121
|
+
targetPort: 80,
|
|
122
|
+
deviceIdHex: "5365baf29cb7ab58de588dfc448913cb609283e2",
|
|
123
|
+
protocol: "tls" // Optional - defaults to TLS if not specified
|
|
124
|
+
},
|
|
125
|
+
3003: {
|
|
126
|
+
targetPort: 443,
|
|
127
|
+
deviceIdHex: "0x5365baf29cb7ab58de588dfc448913cb609283e2",
|
|
128
|
+
protocol: "tcp" // Can be "tls", "tcp", or "udp"
|
|
129
|
+
}
|
|
73
130
|
};
|
|
74
131
|
|
|
75
132
|
const portForward = new BindPort(connection, portsConfig);
|
|
76
133
|
portForward.bind();
|
|
77
134
|
|
|
78
|
-
// You can also dynamically add
|
|
79
|
-
portForward.addPort(3004, 8080, "5365baf29cb7ab58de588dfc448913cb609283e2");
|
|
135
|
+
// You can also dynamically add ports with protocol specification
|
|
136
|
+
portForward.addPort(3004, 8080, "5365baf29cb7ab58de588dfc448913cb609283e2", "udp");
|
|
80
137
|
portForward.removePort(3003);
|
|
81
138
|
}
|
|
82
139
|
|
|
@@ -95,7 +152,7 @@ async function main() {
|
|
|
95
152
|
const connection = new DiodeConnection(host, port, keyLocation);
|
|
96
153
|
await connection.connect();
|
|
97
154
|
|
|
98
|
-
// Legacy method - single port binding
|
|
155
|
+
// Legacy method - single port binding (defaults to TLS protocol)
|
|
99
156
|
const portForward = new BindPort(connection, 3002, 80, "5365baf29cb7ab58de588dfc448913cb609283e2");
|
|
100
157
|
portForward.bind();
|
|
101
158
|
}
|
|
@@ -157,6 +214,16 @@ main();
|
|
|
157
214
|
- `createTicketCommand()`: Creates a ticket command for authentication. Returns a promise.
|
|
158
215
|
- `close()`: Closes the connection to the Diode server.
|
|
159
216
|
- `getDeviceCertificate()`: Returns the generated certificate PEM.
|
|
217
|
+
- `setReconnectOptions(options)`: Configures reconnection behavior with the following options:
|
|
218
|
+
- `maxRetries` (number): Maximum reconnection attempts (default: Infinity)
|
|
219
|
+
- `retryDelay` (number): Initial delay between retries in ms (default: 1000)
|
|
220
|
+
- `maxRetryDelay` (number): Maximum delay between retries in ms (default: 30000)
|
|
221
|
+
- `autoReconnect` (boolean): Whether to automatically reconnect on disconnection (default: true)
|
|
222
|
+
|
|
223
|
+
- **Events**:
|
|
224
|
+
- `reconnecting`: Emitted when a reconnection attempt is about to start, with `attempt` and `delay` information
|
|
225
|
+
- `reconnected`: Emitted when reconnection is successful
|
|
226
|
+
- `reconnect_failed`: Emitted when all reconnection attempts have failed
|
|
160
227
|
|
|
161
228
|
#### `DiodeRPC`
|
|
162
229
|
|
|
@@ -190,13 +257,15 @@ main();
|
|
|
190
257
|
New Constructor:
|
|
191
258
|
- `new BindPort(connection, portsConfig)`
|
|
192
259
|
- `connection` (DiodeConnection): An instance of `DiodeConnection`.
|
|
193
|
-
- `portsConfig` (object): A configuration object where keys are local ports and values are objects with
|
|
194
|
-
|
|
195
|
-
|
|
260
|
+
- `portsConfig` (object): A configuration object where keys are local ports and values are objects with:
|
|
261
|
+
- `targetPort` (number): The target port on the device.
|
|
262
|
+
- `deviceIdHex` (string): The device ID in hexadecimal format (with or without '0x' prefix).
|
|
263
|
+
- `protocol` (string, optional): The protocol to use ("tls", "tcp", or "udp"). Defaults to "tls".
|
|
196
264
|
|
|
197
265
|
- **Methods**:
|
|
198
266
|
- `bind()`: Binds all configured local ports to their target ports on the devices.
|
|
199
|
-
- `addPort(localPort, targetPort, deviceIdHex)`: Adds a new port binding configuration.
|
|
267
|
+
- `addPort(localPort, targetPort, deviceIdHex, protocol)`: Adds a new port binding configuration.
|
|
268
|
+
- `protocol` (string, optional): The protocol to use. Can be "tls", "tcp", or "udp". Defaults to "tls".
|
|
200
269
|
- `removePort(localPort)`: Removes a port binding configuration.
|
|
201
270
|
- `bindSinglePort(localPort)`: Binds a single local port to its target.
|
|
202
271
|
- `closeAllServers()`: Closes all active server instances.
|
package/bindPort.js
CHANGED
|
@@ -1,8 +1,45 @@
|
|
|
1
1
|
const net = require('net');
|
|
2
|
+
const tls = require('tls');
|
|
3
|
+
const dgram = require('dgram');
|
|
2
4
|
const { Buffer } = require('buffer');
|
|
5
|
+
const { Duplex } = require('stream');
|
|
3
6
|
const DiodeRPC = require('./rpc');
|
|
4
7
|
const logger = require('./logger');
|
|
5
8
|
|
|
9
|
+
// Custom Duplex stream to handle the Diode connection
|
|
10
|
+
class DiodeSocket extends Duplex {
|
|
11
|
+
constructor(ref, rpc) {
|
|
12
|
+
super();
|
|
13
|
+
this.ref = ref;
|
|
14
|
+
this.rpc = rpc;
|
|
15
|
+
this.destroyed = false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
_write(chunk, encoding, callback) {
|
|
19
|
+
// Send data to the remote device via portSend
|
|
20
|
+
this.rpc.portSend(this.ref, chunk)
|
|
21
|
+
.then(() => callback())
|
|
22
|
+
.catch((err) => callback(err));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
_read(size) {
|
|
26
|
+
// No need to implement this method for our use case
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Method to push data received from the remote device
|
|
30
|
+
pushData(data) {
|
|
31
|
+
if (!this.destroyed) {
|
|
32
|
+
this.push(data);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
_destroy(err, callback) {
|
|
37
|
+
this.destroyed = true;
|
|
38
|
+
this.push(null);
|
|
39
|
+
callback(err);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
6
43
|
class BindPort {
|
|
7
44
|
constructor(connection, localPortOrPortsConfig, targetPort, deviceIdHex) {
|
|
8
45
|
this.connection = connection;
|
|
@@ -12,7 +49,8 @@ class BindPort {
|
|
|
12
49
|
this.portsConfig = {
|
|
13
50
|
[localPortOrPortsConfig]: {
|
|
14
51
|
targetPort,
|
|
15
|
-
deviceIdHex: this._stripHexPrefix(deviceIdHex)
|
|
52
|
+
deviceIdHex: this._stripHexPrefix(deviceIdHex),
|
|
53
|
+
protocol: 'tls' // Default protocol is tls
|
|
16
54
|
}
|
|
17
55
|
};
|
|
18
56
|
} else {
|
|
@@ -20,10 +58,17 @@ class BindPort {
|
|
|
20
58
|
this.portsConfig = localPortOrPortsConfig || {};
|
|
21
59
|
|
|
22
60
|
// Strip 0x prefix from all deviceIdHex values in portsConfig
|
|
61
|
+
// And ensure protocol is specified (default to tls)
|
|
23
62
|
for (const port in this.portsConfig) {
|
|
24
63
|
if (this.portsConfig[port].deviceIdHex) {
|
|
25
64
|
this.portsConfig[port].deviceIdHex = this._stripHexPrefix(this.portsConfig[port].deviceIdHex);
|
|
26
65
|
}
|
|
66
|
+
// Set default protocol if not provided
|
|
67
|
+
if (!this.portsConfig[port].protocol) {
|
|
68
|
+
this.portsConfig[port].protocol = 'tls';
|
|
69
|
+
}
|
|
70
|
+
// Ensure protocol is uppercase
|
|
71
|
+
this.portsConfig[port].protocol = this.portsConfig[port].protocol.toLowerCase();
|
|
27
72
|
}
|
|
28
73
|
}
|
|
29
74
|
|
|
@@ -45,7 +90,6 @@ class BindPort {
|
|
|
45
90
|
_setupMessageListener() {
|
|
46
91
|
// Listen for data events from the device
|
|
47
92
|
this.connection.on('unsolicited', (message) => {
|
|
48
|
-
// message is [messageId, [messageType, ...]]
|
|
49
93
|
const [messageIdRaw, messageContent] = message;
|
|
50
94
|
const messageTypeRaw = messageContent[0];
|
|
51
95
|
const messageType = Buffer.from(messageTypeRaw).toString('utf8');
|
|
@@ -60,7 +104,13 @@ class BindPort {
|
|
|
60
104
|
// Find the associated client socket from connection
|
|
61
105
|
const clientSocket = this.connection.getClientSocket(dataRef);
|
|
62
106
|
if (clientSocket) {
|
|
63
|
-
clientSocket.
|
|
107
|
+
if (clientSocket.diodeSocket) {
|
|
108
|
+
// If it's a DiodeSocket, push data to it so tls can process
|
|
109
|
+
clientSocket.diodeSocket.pushData(data);
|
|
110
|
+
} else {
|
|
111
|
+
// Otherwise write directly to the socket
|
|
112
|
+
clientSocket.write(data);
|
|
113
|
+
}
|
|
64
114
|
} else {
|
|
65
115
|
const connectionInfo = this.connection.getConnection(dataRef);
|
|
66
116
|
if (connectionInfo) {
|
|
@@ -76,6 +126,9 @@ class BindPort {
|
|
|
76
126
|
// Close the associated client socket
|
|
77
127
|
const clientSocket = this.connection.getClientSocket(dataRef);
|
|
78
128
|
if (clientSocket) {
|
|
129
|
+
if (clientSocket.diodeSocket) {
|
|
130
|
+
clientSocket.diodeSocket._destroy(null, () => {});
|
|
131
|
+
}
|
|
79
132
|
clientSocket.end();
|
|
80
133
|
this.connection.deleteClientSocket(dataRef);
|
|
81
134
|
logger.info(`Port closed for ref: ${dataRef.toString('hex')}`);
|
|
@@ -100,7 +153,7 @@ class BindPort {
|
|
|
100
153
|
});
|
|
101
154
|
}
|
|
102
155
|
|
|
103
|
-
addPort(localPort, targetPort, deviceIdHex) {
|
|
156
|
+
addPort(localPort, targetPort, deviceIdHex, protocol = 'tls') {
|
|
104
157
|
if (this.servers.has(localPort)) {
|
|
105
158
|
logger.warn(`Port ${localPort} is already bound`);
|
|
106
159
|
return false;
|
|
@@ -108,7 +161,8 @@ class BindPort {
|
|
|
108
161
|
|
|
109
162
|
this.portsConfig[localPort] = {
|
|
110
163
|
targetPort,
|
|
111
|
-
deviceIdHex: this._stripHexPrefix(deviceIdHex)
|
|
164
|
+
deviceIdHex: this._stripHexPrefix(deviceIdHex),
|
|
165
|
+
protocol: protocol.toLowerCase()
|
|
112
166
|
};
|
|
113
167
|
|
|
114
168
|
this.bindSinglePort(localPort);
|
|
@@ -151,70 +205,191 @@ class BindPort {
|
|
|
151
205
|
return false;
|
|
152
206
|
}
|
|
153
207
|
|
|
154
|
-
const { targetPort, deviceIdHex } = config;
|
|
208
|
+
const { targetPort, deviceIdHex, protocol = 'tls' } = config;
|
|
155
209
|
const deviceId = Buffer.from(deviceIdHex, 'hex');
|
|
156
210
|
|
|
157
|
-
//
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
211
|
+
// Format the target port with protocol prefix for the remote connection
|
|
212
|
+
const formattedTargetPort = `${protocol}:${targetPort}`;
|
|
213
|
+
logger.info(`Binding local port ${localPort} to remote ${formattedTargetPort}`);
|
|
214
|
+
|
|
215
|
+
// For udp protocol, use udp server
|
|
216
|
+
if (protocol === 'udp') {
|
|
217
|
+
const server = dgram.createSocket('udp4');
|
|
218
|
+
|
|
219
|
+
server.on('listening', () => {
|
|
220
|
+
logger.info(`udp server listening on port ${localPort} forwarding to device port ${targetPort}`);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
server.on('message', async (data, rinfo) => {
|
|
224
|
+
// Open a new port on the device if this is a new client
|
|
225
|
+
const clientKey = `${rinfo.address}:${rinfo.port}`;
|
|
226
|
+
let ref = server.clientRefs && server.clientRefs[clientKey];
|
|
227
|
+
|
|
165
228
|
if (!ref) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
229
|
+
try {
|
|
230
|
+
ref = await this.rpc.portOpen(deviceId, formattedTargetPort, 'rw');
|
|
231
|
+
if (!ref) {
|
|
232
|
+
logger.error(`Error opening port ${formattedTargetPort} on deviceId: ${deviceIdHex}`);
|
|
233
|
+
return;
|
|
234
|
+
} else {
|
|
235
|
+
logger.info(`Port ${formattedTargetPort} opened on device with ref: ${ref.toString('hex')} for udp client ${clientKey}`);
|
|
236
|
+
if (!server.clientRefs) server.clientRefs = {};
|
|
237
|
+
server.clientRefs[clientKey] = ref;
|
|
238
|
+
|
|
239
|
+
// Store the client info
|
|
240
|
+
this.connection.addClientSocket(ref, {
|
|
241
|
+
address: rinfo.address,
|
|
242
|
+
port: rinfo.port,
|
|
243
|
+
protocol: 'udp',
|
|
244
|
+
write: (data) => {
|
|
245
|
+
server.send(data, rinfo.port, rinfo.address);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
} catch (error) {
|
|
250
|
+
logger.error(`Error opening port ${formattedTargetPort} on device: ${error}`);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
171
253
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
clientSocket.destroy();
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Store the client socket with the ref using connection's method
|
|
179
|
-
this.connection.addClientSocket(ref, clientSocket);
|
|
180
|
-
|
|
181
|
-
// When data is received from the client, send it to the device
|
|
182
|
-
clientSocket.on('data', async (data) => {
|
|
254
|
+
|
|
255
|
+
// Send data to the device
|
|
183
256
|
try {
|
|
184
257
|
await this.rpc.portSend(ref, data);
|
|
185
258
|
} catch (error) {
|
|
186
|
-
logger.error(`Error sending data to device: ${error}`);
|
|
187
|
-
clientSocket.destroy();
|
|
259
|
+
logger.error(`Error sending udp data to device: ${error}`);
|
|
188
260
|
}
|
|
189
261
|
});
|
|
262
|
+
|
|
263
|
+
server.on('error', (err) => {
|
|
264
|
+
logger.error(`udp Server error: ${err}`);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
server.bind(localPort);
|
|
268
|
+
this.servers.set(parseInt(localPort), server);
|
|
269
|
+
} else {
|
|
270
|
+
// For TCP and tls protocols, use TCP server locally
|
|
271
|
+
const server = net.createServer(async (clientSocket) => {
|
|
272
|
+
logger.info(`Client connected to local server on port ${localPort}`);
|
|
190
273
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
274
|
+
// Open a new port on the device for this client
|
|
275
|
+
let ref;
|
|
276
|
+
try {
|
|
277
|
+
ref = await this.rpc.portOpen(deviceId, formattedTargetPort, 'rw');
|
|
278
|
+
if (!ref) {
|
|
279
|
+
logger.error(`Error opening port ${formattedTargetPort} on deviceId: ${deviceIdHex}`);
|
|
280
|
+
clientSocket.destroy();
|
|
281
|
+
return;
|
|
282
|
+
} else {
|
|
283
|
+
logger.info(`Port ${formattedTargetPort} opened on device with ref: ${ref.toString('hex')} for client`);
|
|
284
|
+
}
|
|
285
|
+
} catch (error) {
|
|
286
|
+
logger.error(`Error opening port ${formattedTargetPort} on device: ${error}`);
|
|
287
|
+
clientSocket.destroy();
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (protocol === 'tls') {
|
|
292
|
+
// For tls protocol, create a proper tls connection
|
|
195
293
|
try {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
294
|
+
// Create a DiodeSocket to handle communication with the device
|
|
295
|
+
const diodeSocket = new DiodeSocket(ref, this.rpc);
|
|
296
|
+
|
|
297
|
+
// Get the device certificate for tls
|
|
298
|
+
const certPem = this.connection.getDeviceCertificate();
|
|
299
|
+
if (!certPem) {
|
|
300
|
+
throw new Error('No device certificate available');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Setup tls options for client mode
|
|
304
|
+
const tlsOptions = {
|
|
305
|
+
cert: certPem,
|
|
306
|
+
key: certPem,
|
|
307
|
+
rejectUnauthorized: false,
|
|
308
|
+
ciphers: 'ECDHE-ECDSA-AES256-GCM-SHA384',
|
|
309
|
+
ecdhCurve: 'secp256k1',
|
|
310
|
+
minVersion: 'TLSv1.2',
|
|
311
|
+
maxVersion: 'TLSv1.2',
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// Create tls socket as client (not server)
|
|
315
|
+
const tlsSocket = tls.connect({
|
|
316
|
+
socket: diodeSocket,
|
|
317
|
+
...tlsOptions
|
|
318
|
+
}, () => {
|
|
319
|
+
logger.info(`tls connection established to device ${deviceIdHex}`);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// Pipe data between the client socket and the tls socket
|
|
323
|
+
tlsSocket.pipe(clientSocket).pipe(tlsSocket);
|
|
324
|
+
|
|
325
|
+
// Handle tls socket errors
|
|
326
|
+
tlsSocket.on('error', (err) => {
|
|
327
|
+
logger.error(`tls Socket error: ${err}`);
|
|
328
|
+
clientSocket.destroy();
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// Store reference to the diodeSocket so we can push data to it
|
|
332
|
+
const socketWrapper = {
|
|
333
|
+
diodeSocket,
|
|
334
|
+
tlsSocket,
|
|
335
|
+
end: () => {
|
|
336
|
+
tlsSocket.end();
|
|
337
|
+
diodeSocket._destroy(null, () => {});
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
// Store the socket wrapper
|
|
342
|
+
this.connection.addClientSocket(ref, socketWrapper);
|
|
343
|
+
|
|
199
344
|
} catch (error) {
|
|
200
|
-
logger.error(`Error
|
|
345
|
+
logger.error(`Error setting up tls connection: ${error}`);
|
|
346
|
+
clientSocket.destroy();
|
|
347
|
+
return;
|
|
201
348
|
}
|
|
202
349
|
} else {
|
|
203
|
-
|
|
350
|
+
// For TCP protocol, just use the raw socket
|
|
351
|
+
this.connection.addClientSocket(ref, clientSocket);
|
|
352
|
+
|
|
353
|
+
// Handle data from client to device
|
|
354
|
+
clientSocket.on('data', async (data) => {
|
|
355
|
+
try {
|
|
356
|
+
await this.rpc.portSend(ref, data);
|
|
357
|
+
} catch (error) {
|
|
358
|
+
logger.error(`Error sending data to device: ${error}`);
|
|
359
|
+
clientSocket.destroy();
|
|
360
|
+
}
|
|
361
|
+
});
|
|
204
362
|
}
|
|
205
|
-
});
|
|
206
363
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
364
|
+
// Handle client socket closure (common for all protocols)
|
|
365
|
+
clientSocket.on('end', async () => {
|
|
366
|
+
logger.info('Client disconnected');
|
|
367
|
+
if (ref && this.connection.hasClientSocket(ref)) {
|
|
368
|
+
try {
|
|
369
|
+
await this.rpc.portClose(ref);
|
|
370
|
+
logger.info(`Port closed on device for ref: ${ref.toString('hex')}`);
|
|
371
|
+
this.connection.deleteClientSocket(ref);
|
|
372
|
+
} catch (error) {
|
|
373
|
+
logger.error(`Error closing port on device: ${error}`);
|
|
374
|
+
}
|
|
375
|
+
} else {
|
|
376
|
+
logger.warn('Ref is invalid or no longer in clientSockets.');
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// Handle client socket errors
|
|
381
|
+
clientSocket.on('error', (err) => {
|
|
382
|
+
logger.error(`Client socket error: ${err}`);
|
|
383
|
+
});
|
|
210
384
|
});
|
|
211
|
-
});
|
|
212
385
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
386
|
+
server.listen(localPort, () => {
|
|
387
|
+
logger.info(`Local server listening on port ${localPort} forwarding to device ${protocol} port ${targetPort}`);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
this.servers.set(parseInt(localPort), server);
|
|
391
|
+
}
|
|
216
392
|
|
|
217
|
-
this.servers.set(parseInt(localPort), server);
|
|
218
393
|
return true;
|
|
219
394
|
}
|
|
220
395
|
|
package/connection.js
CHANGED
|
@@ -13,6 +13,8 @@ const DiodeRPC = require('./rpc');
|
|
|
13
13
|
const abi = require('ethereumjs-abi');
|
|
14
14
|
const logger = require('./logger');
|
|
15
15
|
const path = require('path');
|
|
16
|
+
// Add dotenv for environment variables
|
|
17
|
+
require('dotenv').config();
|
|
16
18
|
|
|
17
19
|
class DiodeConnection extends EventEmitter {
|
|
18
20
|
constructor(host, port, keyLocation = './db/keys.json') {
|
|
@@ -37,9 +39,38 @@ class DiodeConnection extends EventEmitter {
|
|
|
37
39
|
this.certPem = null;
|
|
38
40
|
// Load or generate keypair
|
|
39
41
|
this.keyPair = loadOrGenerateKeyPair(this.keyLocation);
|
|
42
|
+
|
|
43
|
+
// Load reconnection properties from environment variables with defaults
|
|
44
|
+
const envMaxRetries = process.env.DIODE_MAX_RETRIES;
|
|
45
|
+
this.maxRetries = envMaxRetries !== undefined ?
|
|
46
|
+
(envMaxRetries.toLowerCase() === 'infinity' ? Infinity : parseInt(envMaxRetries, 10)) :
|
|
47
|
+
Infinity;
|
|
48
|
+
|
|
49
|
+
this.retryDelay = parseInt(process.env.DIODE_RETRY_DELAY, 10) || 1000; // Default: 1 second
|
|
50
|
+
this.maxRetryDelay = parseInt(process.env.DIODE_MAX_RETRY_DELAY, 10) || 30000; // Default: 30 seconds
|
|
51
|
+
|
|
52
|
+
// Parse boolean from string ('true'/'false')
|
|
53
|
+
const envAutoReconnect = process.env.DIODE_AUTO_RECONNECT;
|
|
54
|
+
this.autoReconnect = envAutoReconnect !== undefined ?
|
|
55
|
+
(envAutoReconnect.toLowerCase() === 'true') :
|
|
56
|
+
true;
|
|
57
|
+
|
|
58
|
+
this.retryCount = 0;
|
|
59
|
+
this.retryTimeoutId = null;
|
|
60
|
+
|
|
61
|
+
// Log the reconnection settings
|
|
62
|
+
logger.info(`Connection settings - Auto Reconnect: ${this.autoReconnect}, Max Retries: ${
|
|
63
|
+
this.maxRetries === Infinity ? 'Infinity' : this.maxRetries
|
|
64
|
+
}, Retry Delay: ${this.retryDelay}ms, Max Retry Delay: ${this.maxRetryDelay}ms`);
|
|
40
65
|
}
|
|
41
66
|
|
|
42
67
|
connect() {
|
|
68
|
+
// Clear any existing retry timeout
|
|
69
|
+
if (this.retryTimeoutId) {
|
|
70
|
+
clearTimeout(this.retryTimeoutId);
|
|
71
|
+
this.retryTimeoutId = null;
|
|
72
|
+
}
|
|
73
|
+
|
|
43
74
|
return new Promise((resolve, reject) => {
|
|
44
75
|
// Generate a temporary certificate valid for 1 month
|
|
45
76
|
this.certPem = generateCert(this.keyPair.prvKeyObj, this.keyPair.pubKeyObj);
|
|
@@ -56,8 +87,10 @@ class DiodeConnection extends EventEmitter {
|
|
|
56
87
|
|
|
57
88
|
this.socket = tls.connect(this.port, this.host, options, async () => {
|
|
58
89
|
logger.info('Connected to Diode.io server');
|
|
90
|
+
// Reset retry counter on successful connection
|
|
91
|
+
this.retryCount = 0;
|
|
59
92
|
// Set keep-alive to prevent connection timeout forever
|
|
60
|
-
this.socket.setKeepAlive(true,
|
|
93
|
+
this.socket.setKeepAlive(true, 1500);
|
|
61
94
|
|
|
62
95
|
// Send the ticketv2 command
|
|
63
96
|
try {
|
|
@@ -79,14 +112,87 @@ class DiodeConnection extends EventEmitter {
|
|
|
79
112
|
logger.error(`Error handling data: ${error}`);
|
|
80
113
|
}
|
|
81
114
|
});
|
|
115
|
+
|
|
82
116
|
this.socket.on('error', (err) => {
|
|
83
117
|
logger.error(`Connection error: ${err}`);
|
|
84
|
-
|
|
118
|
+
reject(err);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
this.socket.on('end', () => {
|
|
122
|
+
logger.info('Disconnected from server');
|
|
123
|
+
this._handleDisconnect();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
this.socket.on('close', (hadError) => {
|
|
127
|
+
logger.info(`Connection closed${hadError ? ' due to error' : ''}`);
|
|
128
|
+
this._handleDisconnect();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
this.socket.on('timeout', () => {
|
|
132
|
+
logger.warn('Connection timeout');
|
|
133
|
+
this._handleDisconnect();
|
|
85
134
|
});
|
|
86
|
-
this.socket.on('end', () => logger.info('Disconnected from server'));
|
|
87
135
|
});
|
|
88
136
|
}
|
|
89
137
|
|
|
138
|
+
// New method to handle reconnection with exponential backoff
|
|
139
|
+
_reconnect() {
|
|
140
|
+
if (!this.autoReconnect || this.isReconnecting) return;
|
|
141
|
+
|
|
142
|
+
this.retryCount++;
|
|
143
|
+
|
|
144
|
+
if (this.maxRetries !== Infinity && this.retryCount > this.maxRetries) {
|
|
145
|
+
logger.error(`Maximum reconnection attempts (${this.maxRetries}) reached. Giving up.`);
|
|
146
|
+
this.emit('reconnect_failed');
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Calculate delay with exponential backoff
|
|
151
|
+
const delay = Math.min(this.retryDelay * Math.pow(1.5, this.retryCount - 1), this.maxRetryDelay);
|
|
152
|
+
|
|
153
|
+
logger.info(`Reconnecting in ${delay}ms... (Attempt ${this.retryCount})`);
|
|
154
|
+
this.emit('reconnecting', { attempt: this.retryCount, delay });
|
|
155
|
+
|
|
156
|
+
this.retryTimeoutId = setTimeout(() => {
|
|
157
|
+
this.isReconnecting = true;
|
|
158
|
+
|
|
159
|
+
// Clear existing socket if any
|
|
160
|
+
if (this.socket) {
|
|
161
|
+
this.socket.removeAllListeners();
|
|
162
|
+
if (!this.socket.destroyed) {
|
|
163
|
+
this.socket.destroy();
|
|
164
|
+
}
|
|
165
|
+
this.socket = null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Connect again
|
|
169
|
+
this.connect()
|
|
170
|
+
.then(() => {
|
|
171
|
+
this.isReconnecting = false;
|
|
172
|
+
this.emit('reconnected');
|
|
173
|
+
logger.info('Successfully reconnected to Diode.io server');
|
|
174
|
+
})
|
|
175
|
+
.catch((err) => {
|
|
176
|
+
this.isReconnecting = false;
|
|
177
|
+
logger.error(`Reconnection attempt failed: ${err}`);
|
|
178
|
+
});
|
|
179
|
+
}, delay);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Helper method to handle disconnection events
|
|
183
|
+
_handleDisconnect() {
|
|
184
|
+
// Reset socket to null to ensure we don't try to use it
|
|
185
|
+
if (this.socket && !this.socket.destroyed) {
|
|
186
|
+
this.socket.destroy();
|
|
187
|
+
}
|
|
188
|
+
this.socket = null;
|
|
189
|
+
|
|
190
|
+
// Don't try to reconnect if we're intentionally closing
|
|
191
|
+
if (this.autoReconnect && !this.isReconnecting) {
|
|
192
|
+
this._reconnect();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
90
196
|
_ensureConnected() {
|
|
91
197
|
if (this.socket && !this.socket.destroyed) {
|
|
92
198
|
return Promise.resolve();
|
|
@@ -94,20 +200,58 @@ class DiodeConnection extends EventEmitter {
|
|
|
94
200
|
if (this.connectPromise) {
|
|
95
201
|
return this.connectPromise;
|
|
96
202
|
}
|
|
97
|
-
|
|
98
|
-
this.connectPromise =
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
203
|
+
|
|
204
|
+
this.connectPromise = new Promise((resolve, reject) => {
|
|
205
|
+
|
|
206
|
+
if (this.isReconnecting) {
|
|
207
|
+
// If we're already reconnecting, wait for the reconnection to complete
|
|
208
|
+
this.once('reconnected', resolve);
|
|
209
|
+
//wait for max retry delay
|
|
210
|
+
setTimeout(() => {
|
|
211
|
+
reject(new Error('Reconnection timed out'));
|
|
212
|
+
}, this.maxRetryDelay);
|
|
213
|
+
} else {
|
|
214
|
+
this._reconnect();
|
|
215
|
+
this.once('reconnected', resolve);
|
|
216
|
+
//wait for max retry delay
|
|
217
|
+
setTimeout(() => {
|
|
218
|
+
reject(new Error('Reconnection timed out'));
|
|
219
|
+
}, this.maxRetryDelay);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
108
223
|
return this.connectPromise;
|
|
109
224
|
}
|
|
110
225
|
|
|
226
|
+
// Method to set reconnection options
|
|
227
|
+
setReconnectOptions(options = {}) {
|
|
228
|
+
if (typeof options.maxRetries === 'number') {
|
|
229
|
+
this.maxRetries = options.maxRetries;
|
|
230
|
+
}
|
|
231
|
+
if (typeof options.retryDelay === 'number') {
|
|
232
|
+
this.retryDelay = options.retryDelay;
|
|
233
|
+
}
|
|
234
|
+
if (typeof options.maxRetryDelay === 'number') {
|
|
235
|
+
this.maxRetryDelay = options.maxRetryDelay;
|
|
236
|
+
}
|
|
237
|
+
if (typeof options.autoReconnect === 'boolean') {
|
|
238
|
+
this.autoReconnect = options.autoReconnect;
|
|
239
|
+
}
|
|
240
|
+
return this;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Update close method to prevent reconnection when intentionally closing
|
|
244
|
+
close() {
|
|
245
|
+
this.autoReconnect = false;
|
|
246
|
+
if (this.retryTimeoutId) {
|
|
247
|
+
clearTimeout(this.retryTimeoutId);
|
|
248
|
+
this.retryTimeoutId = null;
|
|
249
|
+
}
|
|
250
|
+
if (this.socket) {
|
|
251
|
+
this.socket.end();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
111
255
|
_handleData(data) {
|
|
112
256
|
// Append new data to the receive buffer
|
|
113
257
|
this.receiveBuffer = Buffer.concat([this.receiveBuffer, data]);
|
|
@@ -428,10 +572,6 @@ class DiodeConnection extends EventEmitter {
|
|
|
428
572
|
return this.requestId;
|
|
429
573
|
}
|
|
430
574
|
|
|
431
|
-
close() {
|
|
432
|
-
this.socket.end();
|
|
433
|
-
}
|
|
434
|
-
|
|
435
575
|
// Client sockets management methods (for BindPort)
|
|
436
576
|
addClientSocket(ref, socket) {
|
|
437
577
|
this.clientSockets.set(ref.toString('hex'), socket);
|
|
@@ -9,8 +9,8 @@ async function main() {
|
|
|
9
9
|
await connection.connect();
|
|
10
10
|
|
|
11
11
|
const portForward = new BindPort(connection, {
|
|
12
|
-
3003: { targetPort: 8080, deviceIdHex: "0xca1e71d8105a598810578fb6042fa8cbc1e7f039" },
|
|
13
|
-
3004: { targetPort:
|
|
12
|
+
3003: { targetPort: 8080, deviceIdHex: "0xca1e71d8105a598810578fb6042fa8cbc1e7f039", protocol: "tcp" },
|
|
13
|
+
3004: { targetPort: 8081, deviceIdHex: "5365baf29cb7ab58de588dfc448913cb609283e2", protocol: "tls" }
|
|
14
14
|
});
|
|
15
15
|
portForward.bind();
|
|
16
16
|
|
|
@@ -23,7 +23,7 @@ async function main() {
|
|
|
23
23
|
// after 10 seconds, add port 3003 back
|
|
24
24
|
setTimeout(() => {
|
|
25
25
|
console.log("Adding port 3003 back");
|
|
26
|
-
portForward.addPort(3003, 8080, "0xca1e71d8105a598810578fb6042fa8cbc1e7f039");
|
|
26
|
+
portForward.addPort(3003, 8080, "0xca1e71d8105a598810578fb6042fa8cbc1e7f039", "tcp");
|
|
27
27
|
}, 10000);
|
|
28
28
|
|
|
29
29
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "diodejs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "A JavaScript client for interacting with the Diode network. It provides functionalities to bind and publish ports, send RPC commands, and handle responses.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
package/publishPort.js
CHANGED
|
@@ -254,7 +254,7 @@ class PublishPort extends EventEmitter {
|
|
|
254
254
|
// Create a DiodeSocket instance
|
|
255
255
|
const diodeSocket = new DiodeSocket(ref, this.rpc);
|
|
256
256
|
|
|
257
|
-
certPem = this.connection.getDeviceCertificate();
|
|
257
|
+
const certPem = this.connection.getDeviceCertificate();
|
|
258
258
|
|
|
259
259
|
// TLS options with your server's certificate and key
|
|
260
260
|
const tlsOptions = {
|