diodejs 0.1.0 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,9 +25,9 @@ const { DiodeConnection, DiodeRPC, makeReadable } = require('diodejs');
25
25
  async function main() {
26
26
  const host = 'eu2.prenet.diode.io';
27
27
  const port = 41046;
28
- const certPath = 'device_certificate.pem';
28
+ const keyLocation = './db/keys.json'; // Optional, defaults to './db/keys.json'
29
29
 
30
- const connection = new DiodeConnection(host, port, certPath);
30
+ const connection = new DiodeConnection(host, port, keyLocation);
31
31
  await connection.connect();
32
32
 
33
33
  const rpc = new DiodeRPC(connection);
@@ -54,24 +54,55 @@ main();
54
54
  ### Bind Port
55
55
  Here's a quick example to get you started with port forwarding using the `BindPort` class.
56
56
 
57
+ #### Port Binding
57
58
  ```javascript
58
59
  const { DiodeConnection, BindPort } = require('diodejs');
59
60
 
60
61
  async function main() {
61
62
  const host = 'eu2.prenet.diode.io';
62
63
  const port = 41046;
63
- const certPath = 'device_certificate.pem';
64
+ const keyLocation = './db/keys.json';
64
65
 
65
- const connection = new DiodeConnection(host, port, certPath);
66
+ const connection = new DiodeConnection(host, port, keyLocation);
66
67
  await connection.connect();
67
68
 
68
- const portForward = new BindPort(connection, 3002, 80, "5365baf29cb7ab58de588dfc448913cb609283e2");
69
+ // Multiple or single port binding with configuration object
70
+ const portsConfig = {
71
+ 3002: { targetPort: 80, deviceIdHex: "5365baf29cb7ab58de588dfc448913cb609283e2" },
72
+ 3003: { targetPort: 443, deviceIdHex: "5365baf29cb7ab58de588dfc448913cb609283e2" }
73
+ };
74
+
75
+ const portForward = new BindPort(connection, portsConfig);
69
76
  portForward.bind();
70
77
 
78
+ // You can also dynamically add and remove ports
79
+ portForward.addPort(3004, 8080, "5365baf29cb7ab58de588dfc448913cb609283e2");
80
+ portForward.removePort(3003);
81
+ }
82
+
83
+ main();
84
+ ```
85
+
86
+ #### Single Port Binding (Legacy)
87
+ ```javascript
88
+ const { DiodeConnection, BindPort } = require('diodejs');
89
+
90
+ async function main() {
91
+ const host = 'eu2.prenet.diode.io';
92
+ const port = 41046;
93
+ const keyLocation = './db/keys.json';
94
+
95
+ const connection = new DiodeConnection(host, port, keyLocation);
96
+ await connection.connect();
97
+
98
+ // Legacy method - single port binding
99
+ const portForward = new BindPort(connection, 3002, 80, "5365baf29cb7ab58de588dfc448913cb609283e2");
100
+ portForward.bind();
71
101
  }
72
102
 
73
103
  main();
74
104
  ```
105
+
75
106
  ### Publish Port
76
107
 
77
108
  Here's a quick example to get you started with publishing ports using the `PublishPort` class:
@@ -82,18 +113,28 @@ const { DiodeConnection, PublishPort } = require('diodejs');
82
113
  async function main() {
83
114
  const host = 'us2.prenet.diode.io';
84
115
  const port = 41046;
85
- const certPath = 'device_certificate.pem';
116
+ const keyLocation = './db/keys.json';
86
117
 
87
- const connection = new DiodeConnection(host, port, certPath);
118
+ const connection = new DiodeConnection(host, port, keyLocation);
88
119
  await connection.connect();
89
120
 
90
- const publishedPorts = [8080]; // Ports you want to publish
91
- const publishPort = new PublishPort(connection, publishedPorts, certPath);
92
-
121
+ // Option 1: Simple array of ports (all public)
122
+ const publishedPorts = [8080, 3000];
123
+
124
+ // Option 2: Object with port configurations for public/private access control
125
+ const publishedPortsWithConfig = {
126
+ 8080: { mode: 'public' }, // Public port, accessible by any device
127
+ 3000: {
128
+ mode: 'private',
129
+ whitelist: ['0x1234abcd5678...', '0x9876fedc5432...'] // Only these devices can connect
130
+ }
131
+ };
132
+
133
+ // certPath parameter is maintained for backward compatibility but not required
134
+ const publishPort = new PublishPort(connection, publishedPortsWithConfig);
93
135
  }
94
136
 
95
137
  main();
96
-
97
138
  ```
98
139
 
99
140
  ## Reference
@@ -102,19 +143,20 @@ main();
102
143
 
103
144
  #### `DiodeConnection`
104
145
 
105
- - **Constructor**: `new DiodeConnection(host, port, certPath)`
146
+ - **Constructor**: `new DiodeConnection(host, port, keyLocation)`
106
147
  - `host` (string): The host address of the Diode server.
107
148
  - `port` (number): The port number of the Diode server.
108
- - `certPath` (string)(default: ./cert/device_certificate.pem): The path to the device certificate. If doesn't exist, generates automaticly.
149
+ - `keyLocation` (string)(default: './db/keys.json'): The path to the key storage file. If the file doesn't exist, keys are generated automatically.
109
150
 
110
151
  - **Methods**:
111
152
  - `connect()`: Connects to the Diode server. Returns a promise.
112
153
  - `sendCommand(commandArray)`: Sends a command to the Diode server. Returns a promise.
113
154
  - `sendCommandWithSessionId(commandArray, sessionId)`: Sends a command with a session ID. Returns a promise.
114
- - `getEthereumAddress()`: Returns the Ethereum address derived from the device certificate.
155
+ - `getEthereumAddress()`: Returns the Ethereum address derived from the device keys.
115
156
  - `getServerEthereumAddress()`: Returns the Ethereum address of the server.
116
157
  - `createTicketCommand()`: Creates a ticket command for authentication. Returns a promise.
117
158
  - `close()`: Closes the connection to the Diode server.
159
+ - `getDeviceCertificate()`: Returns the generated certificate PEM.
118
160
 
119
161
  #### `DiodeRPC`
120
162
 
@@ -136,23 +178,47 @@ main();
136
178
 
137
179
  #### `BindPort`
138
180
 
139
- - **Constructor**: `new BindPort(connection, localPort, targetPort, deviceIdHex)`
140
- - `connection` (DiodeConnection): An instance of `DiodeConnection`.
141
- - `localPort` (number): The local port to bind.
142
- - `targetPort` (number): The target port on the device.
143
- - `deviceIdHex` (string): The device ID in hexadecimal format.
181
+ - **Constructors**:
182
+
183
+ Legacy Constructor:
184
+ - `new BindPort(connection, localPort, targetPort, deviceIdHex)`
185
+ - `connection` (DiodeConnection): An instance of `DiodeConnection`.
186
+ - `localPort` (number): The local port to bind.
187
+ - `targetPort` (number): The target port on the device.
188
+ - `deviceIdHex` (string): The device ID in hexadecimal format.
189
+
190
+ New Constructor:
191
+ - `new BindPort(connection, portsConfig)`
192
+ - `connection` (DiodeConnection): An instance of `DiodeConnection`.
193
+ - `portsConfig` (object): A configuration object where keys are local ports and values are objects with `targetPort` and `deviceIdHex`.
194
+ Example: `{ 3002: { targetPort: 80, deviceIdHex: "5365baf29cb7ab58de588dfc448913cb609283e2" } }`
144
195
 
145
196
  - **Methods**:
146
- - `bind()`: Binds the local port to the target port on the device.
197
+ - `bind()`: Binds all configured local ports to their target ports on the devices.
198
+ - `addPort(localPort, targetPort, deviceIdHex)`: Adds a new port binding configuration.
199
+ - `removePort(localPort)`: Removes a port binding configuration.
200
+ - `bindSinglePort(localPort)`: Binds a single local port to its target.
201
+ - `closeAllServers()`: Closes all active server instances.
147
202
 
148
203
  #### `PublishPort`
149
204
 
150
- - **Constructor**: `new PublishPort(connection, publishedPorts, certPath)`
205
+ - **Constructor**: `new PublishPort(connection, publishedPorts, _certPath)`
151
206
  - `connection` (DiodeConnection): An instance of `DiodeConnection`.
152
- - `publishedPorts` (array): An array of ports to publish.
153
- - `certPath` (string): The path to the device certificate.
207
+ - `publishedPorts` (array|object): Either:
208
+ - An array of ports to publish (all public mode)
209
+ - An object mapping ports to their configuration: `{ port: { mode: 'public'|'private', whitelist: ['0x123...'] } }`
210
+ - `_certPath` (string): Has no functionality and maintained for backward compatibility.
154
211
 
155
212
  - **Methods**:
213
+ - `addPort(port, config)`: Adds a new port to publish. Config is optional and defaults to public mode.
214
+ - `port` (number): The port number to publish.
215
+ - `config` (object): Optional configuration with `mode` ('public'|'private') and `whitelist` array.
216
+ - `removePort(port)`: Removes a published port.
217
+ - `port` (number): The port number to remove.
218
+ - `addPorts(ports)`: Adds multiple ports at once (equivalent to the constructor's publishedPorts parameter).
219
+ - `ports` (array|object): Either an array of port numbers or an object mapping ports to their configurations.
220
+ - `getPublishedPorts()`: Returns a plain object with all published ports and their configurations.
221
+ - `clearPorts()`: Removes all published ports. Returns the number of ports that were cleared.
156
222
  - `startListening()`: Starts listening for unsolicited messages.
157
223
  - `handlePortOpen(sessionIdRaw, messageContent)`: Handles port open requests.
158
224
  - `handlePortSend(sessionIdRaw, messageContent)`: Handles port send requests.
package/bindPort.js CHANGED
@@ -4,18 +4,27 @@ const DiodeRPC = require('./rpc');
4
4
  const logger = require('./logger');
5
5
 
6
6
  class BindPort {
7
- constructor(connection, localPort, targetPort,deviceIdHex) {
7
+ constructor(connection, localPortOrPortsConfig, targetPort, deviceIdHex) {
8
8
  this.connection = connection;
9
- this.localPort = localPort;
10
- this.targetPort = targetPort;
11
- this.deviceIdHex = deviceIdHex;
9
+
10
+ // Handle legacy constructor (connection, localPort, targetPort, deviceIdHex)
11
+ if (typeof localPortOrPortsConfig === 'number' && targetPort !== undefined && deviceIdHex !== undefined) {
12
+ this.portsConfig = {
13
+ [localPortOrPortsConfig]: { targetPort, deviceIdHex }
14
+ };
15
+ } else {
16
+ // New constructor (connection, portsConfig)
17
+ this.portsConfig = localPortOrPortsConfig || {};
18
+ }
19
+
20
+ this.servers = new Map(); // Track server instances by localPort
21
+ this.rpc = new DiodeRPC(this.connection);
22
+
23
+ // Set up listener for unsolicited messages once
24
+ this._setupMessageListener();
12
25
  }
13
-
14
- bind () {
15
- const deviceId = Buffer.from(this.deviceIdHex, 'hex');
16
- // Remove local clientSockets map and use the one from connection
17
- const rpc = new DiodeRPC(this.connection);
18
-
26
+
27
+ _setupMessageListener() {
19
28
  // Listen for data events from the device
20
29
  this.connection.on('unsolicited', (message) => {
21
30
  // message is [messageId, [messageType, ...]]
@@ -44,7 +53,6 @@ class BindPort {
44
53
  }
45
54
  } else if (messageType === 'portclose') {
46
55
  const refRaw = messageContent[1];
47
-
48
56
  const dataRef = Buffer.from(refRaw);
49
57
 
50
58
  // Close the associated client socket
@@ -60,24 +68,91 @@ class BindPort {
60
68
  }
61
69
  }
62
70
  });
71
+
72
+ // Handle device disconnect
73
+ this.connection.on('end', () => {
74
+ logger.info('Disconnected from Diode.io server');
75
+ this.closeAllServers();
76
+ });
63
77
 
78
+ // Handle connection errors
79
+ this.connection.on('error', (err) => {
80
+ logger.error(`Connection error: ${err}`);
81
+ this.closeAllServers();
82
+ });
83
+ }
84
+
85
+ addPort(localPort, targetPort, deviceIdHex) {
86
+ if (this.servers.has(localPort)) {
87
+ logger.warn(`Port ${localPort} is already bound`);
88
+ return false;
89
+ }
90
+
91
+ this.portsConfig[localPort] = { targetPort, deviceIdHex };
92
+
93
+ // If we're already bound, set up this new port immediately
94
+ if (this.servers.size > 0) {
95
+ this.bindSinglePort(localPort);
96
+ }
97
+
98
+ return true;
99
+ }
100
+
101
+ removePort(localPort) {
102
+ if (!this.portsConfig[localPort]) {
103
+ logger.warn(`Port ${localPort} is not configured`);
104
+ return false;
105
+ }
106
+
107
+ // Close the server if it's running
108
+ if (this.servers.has(localPort)) {
109
+ const server = this.servers.get(localPort);
110
+ server.close(() => {
111
+ logger.info(`Server on port ${localPort} closed`);
112
+ });
113
+ this.servers.delete(localPort);
114
+ }
115
+
116
+ // Remove from config
117
+ delete this.portsConfig[localPort];
118
+ return true;
119
+ }
120
+
121
+ closeAllServers() {
122
+ for (const [localPort, server] of this.servers.entries()) {
123
+ server.close();
124
+ logger.info(`Server on port ${localPort} closed`);
125
+ }
126
+ this.servers.clear();
127
+ }
128
+
129
+ bindSinglePort(localPort) {
130
+ const config = this.portsConfig[localPort];
131
+ if (!config) {
132
+ logger.error(`No configuration found for port ${localPort}`);
133
+ return false;
134
+ }
135
+
136
+ const { targetPort, deviceIdHex } = config;
137
+ const deviceId = Buffer.from(deviceIdHex, 'hex');
138
+
64
139
  // Set up local server
65
140
  const server = net.createServer(async (clientSocket) => {
66
- logger.info('Client connected to local server');
141
+ logger.info(`Client connected to local server on port ${localPort}`);
67
142
 
68
143
  // Open a new port on the device for this client
69
144
  let ref;
70
145
  try {
71
- ref = await rpc.portOpen(deviceId, this.targetPort, 'rw');
146
+ ref = await this.rpc.portOpen(deviceId, targetPort, 'rw');
72
147
  if (!ref) {
73
- logger.error('Error opening port on device');
148
+ logger.error(`Error opening port ${targetPort} on deviceId: ${deviceIdHex}`);
74
149
  clientSocket.destroy();
75
- return
150
+ return;
76
151
  } else {
77
- logger.info(`Port opened on device with ref: ${ref.toString('hex')} for client`);
152
+ logger.info(`Port ${targetPort} opened on device with ref: ${ref.toString('hex')} for client`);
78
153
  }
79
154
  } catch (error) {
80
- logger.error(`Error opening port on device: ${error}`);
155
+ logger.error(`Error opening port ${targetPort} on device: ${error}`);
81
156
  clientSocket.destroy();
82
157
  return;
83
158
  }
@@ -88,7 +163,7 @@ class BindPort {
88
163
  // When data is received from the client, send it to the device
89
164
  clientSocket.on('data', async (data) => {
90
165
  try {
91
- await rpc.portSend(ref, data);
166
+ await this.rpc.portSend(ref, data);
92
167
  } catch (error) {
93
168
  logger.error(`Error sending data to device: ${error}`);
94
169
  clientSocket.destroy();
@@ -100,7 +175,7 @@ class BindPort {
100
175
  logger.info('Client disconnected');
101
176
  if (ref && this.connection.hasClientSocket(ref)) {
102
177
  try {
103
- await rpc.portClose(ref);
178
+ await this.rpc.portClose(ref);
104
179
  logger.info(`Port closed on device for ref: ${ref.toString('hex')}`);
105
180
  this.connection.deleteClientSocket(ref);
106
181
  } catch (error) {
@@ -117,21 +192,22 @@ class BindPort {
117
192
  });
118
193
  });
119
194
 
120
- server.listen(this.localPort, () => {
121
- logger.info(`Local server listening on port ${this.localPort}`);
122
- });
123
-
124
- // Handle device disconnect
125
- this.connection.on('end', () => {
126
- logger.info('Disconnected from Diode.io server');
127
- server.close();
195
+ server.listen(localPort, () => {
196
+ logger.info(`Local server listening on port ${localPort} forwarding to device port ${targetPort}`);
128
197
  });
198
+
199
+ this.servers.set(parseInt(localPort), server);
200
+ return true;
201
+ }
129
202
 
130
- // Handle connection errors
131
- this.connection.on('error', (err) => {
132
- logger.error(`Connection error: ${err}`);
133
- server.close();
134
- });
203
+ bind() {
204
+ // Close any existing servers first
205
+ this.closeAllServers();
206
+
207
+ // Create servers for each port in the config
208
+ for (const localPort in this.portsConfig) {
209
+ this.bindSinglePort(parseInt(localPort));
210
+ }
135
211
  }
136
212
  }
137
213
 
package/connection.js CHANGED
@@ -3,7 +3,7 @@ const tls = require('tls');
3
3
  const fs = require('fs');
4
4
  const { RLP } = require('@ethereumjs/rlp');
5
5
  const EventEmitter = require('events');
6
- const { makeReadable, parseRequestId, parseResponseType, parseReason, generateCert } = require('./utils');
6
+ const { makeReadable, parseRequestId, parseResponseType, parseReason, generateCert, ensureDirectoryExistence, loadOrGenerateKeyPair } = require('./utils');
7
7
  const { Buffer } = require('buffer'); // Import Buffer
8
8
  const asn1 = require('asn1.js');
9
9
  const secp256k1 = require('secp256k1');
@@ -12,12 +12,14 @@ const crypto = require('crypto');
12
12
  const DiodeRPC = require('./rpc');
13
13
  const abi = require('ethereumjs-abi');
14
14
  const logger = require('./logger');
15
+ const path = require('path');
16
+
15
17
  class DiodeConnection extends EventEmitter {
16
- constructor(host, port, certPath = './cert/device_certificate.pem') {
18
+ constructor(host, port, keyLocation = './db/keys.json') {
17
19
  super();
18
20
  this.host = host;
19
21
  this.port = port;
20
- this.certPath = certPath;
22
+ this.keyLocation = keyLocation;
21
23
  this.socket = null;
22
24
  this.requestId = 0; // Initialize request ID counter
23
25
  this.pendingRequests = new Map(); // Map to store pending requests
@@ -32,18 +34,19 @@ class DiodeConnection extends EventEmitter {
32
34
  // Add maps for storing client sockets and connections
33
35
  this.clientSockets = new Map(); // For BindPort
34
36
  this.connections = new Map(); // For PublishPort
35
-
36
- // Check if certPath exists, if not generate the certificate
37
- if (!fs.existsSync(this.certPath)) {
38
- generateCert(this.certPath);
39
- }
37
+ this.certPem = null;
38
+ // Load or generate keypair
39
+ this.keyPair = loadOrGenerateKeyPair(this.keyLocation);
40
40
  }
41
41
 
42
42
  connect() {
43
43
  return new Promise((resolve, reject) => {
44
+ // Generate a temporary certificate valid for 1 month
45
+ this.certPem = generateCert(this.keyPair.prvKeyObj, this.keyPair.pubKeyObj);
46
+
44
47
  const options = {
45
- cert: fs.readFileSync(this.certPath),
46
- key: fs.readFileSync(this.certPath),
48
+ cert: this.certPem,
49
+ key: this.certPem,
47
50
  rejectUnauthorized: false,
48
51
  ciphers: 'ECDHE-ECDSA-AES256-GCM-SHA384',
49
52
  ecdhCurve: 'secp256k1',
@@ -276,63 +279,10 @@ class DiodeConnection extends EventEmitter {
276
279
 
277
280
  getEthereumAddress() {
278
281
  try {
279
- const pem = fs.readFileSync(this.certPath, 'utf8');
280
- let privateKeyPem;
281
- let privateKeyDer;
282
- let privateKeyBytes;
283
-
284
- if (pem.includes('-----BEGIN PRIVATE KEY-----')) {
285
- // Handle PKCS#8 format
286
- privateKeyPem = pem
287
- .replace('-----BEGIN PRIVATE KEY-----', '')
288
- .replace('-----END PRIVATE KEY-----', '')
289
- .replace(/\r?\n|\r/g, '');
290
-
291
- privateKeyDer = Buffer.from(privateKeyPem, 'base64');
292
-
293
- // Define ASN.1 structure for PKCS#8 private key
294
- const PrivateKeyInfoASN = asn1.define('PrivateKeyInfo', function () {
295
- this.seq().obj(
296
- this.key('version').int(),
297
- this.key('privateKeyAlgorithm').seq().obj(
298
- this.key('algorithm').objid(),
299
- this.key('parameters').optional()
300
- ),
301
- this.key('privateKey').octstr(),
302
- this.key('attributes').implicit(0).any().optional(),
303
- this.key('publicKey').implicit(1).bitstr().optional()
304
- );
305
- });
306
-
307
- // Decode the DER-encoded private key
308
- const privateKeyInfo = PrivateKeyInfoASN.decode(privateKeyDer, 'der');
309
- const privateKeyOctetString = privateKeyInfo.privateKey;
310
-
311
- // Now parse the ECPrivateKey structure inside the octet string
312
- const ECPrivateKeyASN = asn1.define('ECPrivateKey', function () {
313
- this.seq().obj(
314
- this.key('version').int(),
315
- this.key('privateKey').octstr(),
316
- this.key('parameters').explicit(0).objid().optional(),
317
- this.key('publicKey').explicit(1).bitstr().optional()
318
- );
319
- });
320
-
321
- const ecPrivateKey = ECPrivateKeyASN.decode(privateKeyOctetString, 'der');
322
- privateKeyBytes = ecPrivateKey.privateKey;
323
- logger.debug(`Private key bytes: ${privateKeyBytes.toString('hex')}`);
324
- } else {
325
- throw new Error('Unsupported key format. Expected EC PRIVATE KEY or PRIVATE KEY in PEM format.');
326
- }
327
-
328
- // Compute the public key
329
- const publicKeyUint8Array = secp256k1.publicKeyCreate(privateKeyBytes, false); // uncompressed
330
-
331
- // Convert publicKey to Buffer if necessary
332
- const publicKeyBuffer = Buffer.isBuffer(publicKeyUint8Array)
333
- ? publicKeyUint8Array
334
- : Buffer.from(publicKeyUint8Array);
335
-
282
+ // Use the stored keyPair.pubKeyObj to derive Ethereum address
283
+ const publicKeyDer = this.keyPair.prvKeyObj.generatePublicKeyHex();
284
+ const publicKeyBuffer = Buffer.from(publicKeyDer, 'hex');
285
+
336
286
  // Derive the Ethereum address
337
287
  const addressBuffer = ethUtil.pubToAddress(publicKeyBuffer, true);
338
288
  const address = '0x' + addressBuffer.toString('hex');
@@ -369,106 +319,15 @@ class DiodeConnection extends EventEmitter {
369
319
  }
370
320
  }
371
321
 
372
- // getServerEthereumAddress() {
373
- // try {
374
- // const serverCert = this.socket.getPeerCertificate(true);
375
- // if (!serverCert.raw) {
376
- // throw new Error('Failed to get server certificate.');
377
- // }
378
-
379
- // // Extract public key from the certificate
380
- // const publicKey = serverCert.pubkey; // May need to parse ASN.1 structure to get the public key
381
- // // Assume you have a method to extract the public key buffer from the certificate
382
-
383
- // // Compute Ethereum address from public key
384
- // const publicKeyBuffer = Buffer.from(publicKey); // Ensure it's a Buffer
385
- // const addressBuffer = ethUtil.pubToAddress(publicKeyBuffer, true);
386
-
387
- // return addressBuffer; // Return as Buffer
388
- // } catch (error) {
389
- // console.error('Error extracting server Ethereum address:', error);
390
- // throw error;
391
- // }
392
- // }
393
-
394
- // Method to extract private key bytes from certPath
322
+ // Method to extract private key bytes from keyPair
395
323
  getPrivateKey() {
396
- // Similar to getEthereumAddress(), but return privateKeyBytes
397
- // Ensure to handle different key formats (EC PRIVATE KEY and PRIVATE KEY)
398
324
  try {
399
- const pem = fs.readFileSync(this.certPath, 'utf8');
400
- let privateKeyPem;
401
- let privateKeyDer;
402
- let privateKeyBytes;
403
-
404
- if (pem.includes('-----BEGIN PRIVATE KEY-----')) {
405
- // Handle PKCS#8 format
406
- privateKeyPem = pem
407
- .replace('-----BEGIN PRIVATE KEY-----', '')
408
- .replace('-----END PRIVATE KEY-----', '')
409
- .replace(/\r?\n|\r/g, '');
410
-
411
- privateKeyDer = Buffer.from(privateKeyPem, 'base64');
412
-
413
- // Define ASN.1 structure for PKCS#8 private key
414
- const PrivateKeyInfoASN = asn1.define('PrivateKeyInfo', function () {
415
- this.seq().obj(
416
- this.key('version').int(),
417
- this.key('privateKeyAlgorithm').seq().obj(
418
- this.key('algorithm').objid(),
419
- this.key('parameters').optional()
420
- ),
421
- this.key('privateKey').octstr(),
422
- this.key('attributes').implicit(0).any().optional(),
423
- this.key('publicKey').implicit(1).bitstr().optional()
424
- );
425
- });
426
-
427
- // Decode the DER-encoded private key
428
- const privateKeyInfo = PrivateKeyInfoASN.decode(privateKeyDer, 'der');
429
- const privateKeyOctetString = privateKeyInfo.privateKey;
430
-
431
- // Now parse the ECPrivateKey structure inside the octet string
432
- const ECPrivateKeyASN = asn1.define('ECPrivateKey', function () {
433
- this.seq().obj(
434
- this.key('version').int(),
435
- this.key('privateKey').octstr(),
436
- this.key('parameters').explicit(0).objid().optional(),
437
- this.key('publicKey').explicit(1).bitstr().optional()
438
- );
439
- });
440
-
441
- const ecPrivateKey = ECPrivateKeyASN.decode(privateKeyOctetString, 'der');
442
- privateKeyBytes = ecPrivateKey.privateKey;
443
- } else if (pem.includes('-----BEGIN EC PRIVATE KEY-----')) {
444
- // Handle EC PRIVATE KEY format
445
- privateKeyPem = pem
446
- .replace('-----BEGIN EC PRIVATE KEY-----', '')
447
- .replace('-----END EC PRIVATE KEY-----', '')
448
- .replace(/\r?\n|\r/g, '');
449
-
450
- privateKeyDer = Buffer.from(privateKeyPem, 'base64');
451
-
452
- // Define ASN.1 structure for EC private key
453
- const ECPrivateKeyASN = asn1.define('ECPrivateKey', function () {
454
- this.seq().obj(
455
- this.key('version').int(),
456
- this.key('privateKey').octstr(),
457
- this.key('parameters').explicit(0).objid().optional(),
458
- this.key('publicKey').explicit(1).bitstr().optional()
459
- );
460
- });
461
-
462
- // Decode the DER-encoded private key
463
- const ecPrivateKey = ECPrivateKeyASN.decode(privateKeyDer, 'der');
464
- privateKeyBytes = ecPrivateKey.privateKey;
465
- } else {
466
- throw new Error('Unsupported key format. Expected EC PRIVATE KEY or PRIVATE KEY in PEM format.');
467
- }
468
-
325
+ // Extract private key bytes from the keyPair.prvKeyObj
326
+ const privateKeyHex = this.keyPair.prvKeyObj.prvKeyHex;
327
+ const privateKeyBytes = Buffer.from(privateKeyHex, 'hex');
469
328
  return privateKeyBytes;
470
329
  } catch (error) {
471
- logger.error(`Error extracting Ethereum address: ${error}`);
330
+ logger.error(`Error extracting private key: ${error}`);
472
331
  throw error;
473
332
  }
474
333
  }
@@ -557,6 +416,12 @@ class DiodeConnection extends EventEmitter {
557
416
  return ticketCommand;
558
417
  }
559
418
 
419
+ getDeviceCertificate() {
420
+ return this.certPem;
421
+ }
422
+
423
+
424
+
560
425
  _getNextRequestId() {
561
426
  // Increment the request ID counter, wrap around if necessary
562
427
  this.requestId = (this.requestId + 1) % Number.MAX_SAFE_INTEGER;
@@ -4,9 +4,9 @@ const { makeReadable } = require('../utils');
4
4
  async function main() {
5
5
  const host = 'us2.prenet.diode.io';
6
6
  const port = 41046;
7
- const certPath = 'device_certificate.pem';
7
+ const keyLocation = './db/keys.json';
8
8
 
9
- const connection = new DiodeConnection(host, port, certPath);
9
+ const connection = new DiodeConnection(host, port, keyLocation);
10
10
  await connection.connect();
11
11
  const rpc = connection.RPC;
12
12
 
@@ -3,14 +3,30 @@ const { DiodeConnection, BindPort } = require('../index');
3
3
  async function main() {
4
4
  const host = 'us2.prenet.diode.io';
5
5
  const port = 41046;
6
- const certPath = 'device_certificate.pem';
6
+ const keyLocation = './db/keys.json';
7
7
 
8
- const connection = new DiodeConnection(host, port, certPath);
8
+ const connection = new DiodeConnection(host, port, keyLocation);
9
9
  await connection.connect();
10
10
 
11
- const portForward = new BindPort(connection, 3002, 8080, "5365baf29cb7ab58de588dfc448913cb609283e2");
11
+ const portForward = new BindPort(connection, {
12
+ 3003: { targetPort: 8080, deviceIdHex: "ca1e71d8105a598810578fb6042fa8cbc1e7f039" },
13
+ 3004: { targetPort: 443, deviceIdHex: "5365baf29cb7ab58de588dfc448913cb609283e2" }
14
+ });
12
15
  portForward.bind();
16
+
17
+ // after 5 seconds, remove port 3003
18
+ setTimeout(() => {
19
+ console.log("Removing port 3003");
20
+ portForward.removePort(3003);
21
+ }, 5000);
22
+
23
+ // after 10 seconds, add port 3003 back
24
+ setTimeout(() => {
25
+ console.log("Adding port 3003 back");
26
+ portForward.addPort(3003, 8080, "ca1e71d8105a598810578fb6042fa8cbc1e7f039");
27
+ }, 10000);
28
+
13
29
 
14
30
  }
15
31
 
16
- main();
32
+ main().catch(console.error);
@@ -4,14 +4,14 @@ const BindPort = require('../bindPort')
4
4
 
5
5
  const host = 'us2.prenet.diode.io';
6
6
  const port = 41046;
7
- const certPath = 'device_certificate.pem';
7
+ const keyLocation = './db/keys.json';
8
8
 
9
- const connection = new DiodeConnection(host, port, certPath);
9
+ const connection = new DiodeConnection(host, port, keyLocation);
10
10
 
11
11
  async function main() {
12
12
  await connection.connect();
13
13
  const publishedPorts = [8080]; // Ports you want to publish
14
- const publishPort = new PublishPort(connection, publishedPorts, certPath);
14
+ const publishPort = new PublishPort(connection, publishedPorts);
15
15
 
16
16
  const portForward = new BindPort(connection, 3002, 8080, "5365baf29cb7ab58de588dfc448913cb609283e2");
17
17
  portForward.bind();
@@ -1,19 +1,40 @@
1
1
  // example.js
2
2
 
3
- const DiodeConnection = require('../connection')
4
- const PublishPort = require('../publishPort')
3
+ const { DiodeConnection, PublishPort } = require('../index');
5
4
 
6
5
  async function startPublishing() {
7
6
  const host = 'us2.prenet.diode.io';
8
7
  const port = 41046;
9
- const certPath = 'device_certificate.pem';
8
+ const keyLocation = './db/keys.json';
10
9
 
11
- const connection = new DiodeConnection(host, port, certPath);
10
+ const connection = new DiodeConnection(host, port, keyLocation);
12
11
  await connection.connect();
13
12
 
14
- const publishedPorts = [8080]; // Ports you want to publish
15
- const publishPort = new PublishPort(connection, publishedPorts, certPath);
16
-
13
+ // Create a PublishPort instance with initial ports
14
+ const publishPort = new PublishPort(connection, {
15
+ 3000: { mode: 'public' },
16
+ 8080: {
17
+ mode: 'private',
18
+ whitelist: ['0xca1e71d8105a598810578fb6042fa8cbc1e7f039'] // Replace with actual addresses
19
+ }
20
+ }, keyLocation);
21
+
22
+ console.log('Initial published ports:', publishPort.getPublishedPorts());
23
+
24
+ // After 10 seconds, remove a port
25
+ setTimeout(() => {
26
+ console.log("Removing port 8080");
27
+ publishPort.removePort(3000);
28
+ console.log('Updated published ports:', publishPort.getPublishedPorts());
29
+ }, 10000);
30
+
31
+ // After 15 seconds, add multiple ports
32
+ setTimeout(() => {
33
+ console.log("Adding multiple ports");
34
+ publishPort.addPort(3000,{ mode: 'public' });
35
+ console.log('Updated published ports:', publishPort.getPublishedPorts());
36
+ }, 15000);
37
+
17
38
  }
18
39
 
19
- startPublishing();
40
+ startPublishing().catch(console.error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "diodejs",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
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
@@ -36,14 +36,98 @@ class DiodeSocket extends Duplex {
36
36
  }
37
37
 
38
38
  class PublishPort extends EventEmitter {
39
- constructor(connection, publishedPorts, certPath) {
39
+ constructor(connection, publishedPorts, _certPath = null) {
40
40
  super();
41
41
  this.connection = connection;
42
- this.publishedPorts = new Set(publishedPorts); // Convert array to a Set
43
- // Remove local connections map and use the one from connection
44
- this.startListening();
45
42
  this.rpc = new DiodeRPC(connection);
46
- this.certPath = certPath;
43
+
44
+ // Convert publishedPorts to a Map with configurations
45
+ this.publishedPorts = new Map();
46
+
47
+ // Initialize with the provided ports
48
+ if (publishedPorts) {
49
+ this.addPorts(publishedPorts);
50
+ }
51
+
52
+ this.startListening();
53
+ if (this.publishedPorts.size > 0) {
54
+ logger.info(`Publishing ports: ${Array.from(this.publishedPorts.keys())}`);
55
+ } else {
56
+ logger.info("No ports published initially");
57
+ }
58
+ }
59
+
60
+ // Add a single port with configuration
61
+ addPort(port, config = { mode: 'public', whitelist: [] }) {
62
+ const portNum = parseInt(port, 10);
63
+
64
+ // Normalize the configuration
65
+ const portConfig = {
66
+ mode: config.mode || 'public',
67
+ whitelist: Array.isArray(config.whitelist) ? config.whitelist : []
68
+ };
69
+
70
+ // Add to map
71
+ this.publishedPorts.set(portNum, portConfig);
72
+ logger.info(`Added published port ${portNum} with mode: ${portConfig.mode}`);
73
+
74
+ return true;
75
+ }
76
+
77
+ // Remove a published port
78
+ removePort(port) {
79
+ const portNum = parseInt(port, 10);
80
+
81
+ if (!this.publishedPorts.has(portNum)) {
82
+ logger.warn(`Port ${portNum} is not published`);
83
+ return false;
84
+ }
85
+
86
+ // Close any active connections for this port
87
+ // This could require tracking active connections by port
88
+ // For now, let's log about active connections
89
+ const activeConnections = Array.from(this.connection.connections.values())
90
+ .filter(conn => conn.port === portNum);
91
+
92
+ if (activeConnections.length > 0) {
93
+ logger.warn(`Removing port ${portNum} with ${activeConnections.length} active connections`);
94
+ // We could close these connections, but they'll be rejected naturally on next data transfer
95
+ }
96
+
97
+ this.publishedPorts.delete(portNum);
98
+ logger.info(`Removed published port ${portNum}`);
99
+
100
+ return true;
101
+ }
102
+
103
+ // Add multiple ports at once (from array or object)
104
+ addPorts(ports) {
105
+ if (Array.isArray(ports)) {
106
+ // Legacy array format - treat all ports as public
107
+ ports.forEach(port => {
108
+ this.addPort(port);
109
+ });
110
+ } else if (typeof ports === 'object' && ports !== null) {
111
+ // New object format with configurations
112
+ Object.entries(ports).forEach(([port, config]) => {
113
+ this.addPort(port, config);
114
+ });
115
+ }
116
+
117
+ return this;
118
+ }
119
+
120
+ // Get all published ports with their configurations
121
+ getPublishedPorts() {
122
+ return Object.fromEntries(this.publishedPorts.entries());
123
+ }
124
+
125
+ // Clear all published ports
126
+ clearPorts() {
127
+ const portCount = this.publishedPorts.size;
128
+ this.publishedPorts.clear();
129
+ logger.info(`Cleared ${portCount} published ports`);
130
+ return portCount;
47
131
  }
48
132
 
49
133
  startListening() {
@@ -76,7 +160,7 @@ class PublishPort extends EventEmitter {
76
160
  const sessionId = Buffer.from(sessionIdRaw);
77
161
  const portString = makeReadable(portStringRaw);
78
162
  const ref = Buffer.from(refRaw);
79
- const deviceId = Buffer.from(deviceIdRaw).toString('hex');
163
+ const deviceId = `0x${Buffer.from(deviceIdRaw).toString('hex')}`;
80
164
 
81
165
  logger.info(`Received portopen request for portString ${portString} with ref ${ref.toString('hex')} from device ${deviceId}`);
82
166
 
@@ -87,7 +171,6 @@ class PublishPort extends EventEmitter {
87
171
  port = portString;
88
172
  } else {
89
173
  var [protocol, portStr] = portString.split(':');
90
- console.log(`Protocol: ${protocol}, Port: ${portStr}`);
91
174
  if (!portStr) {
92
175
  portStr = protocol;
93
176
  protocol = 'tcp';
@@ -96,20 +179,31 @@ class PublishPort extends EventEmitter {
96
179
  }
97
180
 
98
181
  // Check if the port is published
99
- if (!this.publishedPorts.has(port)) { // Use .has() instead of .includes()
182
+ if (!this.publishedPorts.has(port)) {
100
183
  logger.warn(`Port ${port} is not published. Rejecting request.`);
101
184
  // Send error response
102
185
  this.rpc.sendError(sessionId, ref, 'Port is not published');
103
186
  return;
104
187
  }
105
188
 
189
+ // Get port configuration and check whitelist if in private mode
190
+ const portConfig = this.publishedPorts.get(port);
191
+ if (portConfig.mode === 'private' && Array.isArray(portConfig.whitelist)) {
192
+ if (!portConfig.whitelist.includes(deviceId)) {
193
+ logger.warn(`Device ${deviceId} is not whitelisted for port ${port}. Rejecting request.`);
194
+ this.rpc.sendError(sessionId, ref, 'Device not whitelisted');
195
+ return;
196
+ }
197
+ logger.info(`Device ${deviceId} is whitelisted for port ${port}. Accepting request.`);
198
+ }
199
+
106
200
  // Handle based on protocol
107
201
  if (protocol === 'tcp') {
108
- this.handleTCPConnection(sessionId, ref, port);
202
+ this.handleTCPConnection(sessionId, ref, port, deviceId);
109
203
  } else if (protocol === 'tls') {
110
- this.handleTLSConnection(sessionId, ref, port);
204
+ this.handleTLSConnection(sessionId, ref, port, deviceId);
111
205
  } else if (protocol === 'udp') {
112
- this.handleUDPConnection(sessionId, ref, port);
206
+ this.handleUDPConnection(sessionId, ref, port, deviceId);
113
207
  } else {
114
208
  logger.warn(`Unsupported protocol: ${protocol}`);
115
209
  this.rpc.sendError(sessionId, ref, `Unsupported protocol: ${protocol}`);
@@ -141,7 +235,7 @@ class PublishPort extends EventEmitter {
141
235
  }
142
236
  }
143
237
 
144
- handleTCPConnection(sessionId, ref, port) {
238
+ handleTCPConnection(sessionId, ref, port, deviceId) {
145
239
  // Create a TCP connection to the local service on the specified port
146
240
  const localSocket = net.connect({ port: port }, () => {
147
241
  logger.info(`Connected to local TCP service on port ${port}`);
@@ -153,17 +247,19 @@ class PublishPort extends EventEmitter {
153
247
  this.setupLocalSocketHandlers(localSocket, ref, 'tcp');
154
248
 
155
249
  // Store the local socket with the ref using connection's method
156
- this.connection.addConnection(ref, { socket: localSocket, protocol: 'tcp' });
250
+ this.connection.addConnection(ref, { socket: localSocket, protocol: 'tcp', port, deviceId });
157
251
  }
158
252
 
159
- handleTLSConnection(sessionId, ref, port) {
253
+ handleTLSConnection(sessionId, ref, port, deviceId) {
160
254
  // Create a DiodeSocket instance
161
255
  const diodeSocket = new DiodeSocket(ref, this.rpc);
162
256
 
257
+ certPem = this.connection.getDeviceCertificate();
258
+
163
259
  // TLS options with your server's certificate and key
164
260
  const tlsOptions = {
165
- cert: fs.readFileSync(this.certPath),
166
- key: fs.readFileSync(this.certPath),
261
+ cert: certPem,
262
+ key: certPem,
167
263
  rejectUnauthorized: false,
168
264
  ciphers: 'ECDHE-ECDSA-AES256-GCM-SHA384',
169
265
  ecdhCurve: 'secp256k1',
@@ -195,7 +291,6 @@ class PublishPort extends EventEmitter {
195
291
  });
196
292
 
197
293
  tlsSocket.on('close', () => {
198
- console.log('TLS Socket closed');
199
294
  this.connection.deleteConnection(ref);
200
295
  });
201
296
 
@@ -205,10 +300,12 @@ class PublishPort extends EventEmitter {
205
300
  tlsSocket,
206
301
  localSocket,
207
302
  protocol: 'tls',
303
+ port,
304
+ deviceId,
208
305
  });
209
306
  }
210
307
 
211
- handleUDPConnection(sessionId, ref, port) {
308
+ handleUDPConnection(sessionId, ref, port, deviceId) {
212
309
  // Create a UDP socket
213
310
  const localSocket = dgram.createSocket('udp4');
214
311
 
@@ -223,6 +320,8 @@ class PublishPort extends EventEmitter {
223
320
  socket: localSocket,
224
321
  protocol: 'udp',
225
322
  remoteInfo,
323
+ port,
324
+ deviceId
226
325
  });
227
326
 
228
327
  logger.info(`UDP connection set up on port ${port}`);
@@ -253,8 +352,26 @@ class PublishPort extends EventEmitter {
253
352
  const data = Buffer.from(dataRaw)//.slice(4);
254
353
 
255
354
  const connectionInfo = this.connection.getConnection(ref);
355
+ // Check if the port is still open and address is still in whitelist
256
356
  if (connectionInfo) {
257
- const { socket: localSocket, protocol, remoteInfo } = connectionInfo;
357
+ const { socket: localSocket, protocol, remoteInfo, port, deviceId } = connectionInfo;
358
+
359
+ if (!this.publishedPorts.has(port)) {
360
+ logger.warn(`Port ${port} is not published. Sending portclose.`);
361
+ this.rpc.portClose(ref);
362
+ this.connection.deleteConnection(ref);
363
+ return;
364
+ }
365
+
366
+ const portConfig = this.publishedPorts.get(port);
367
+ if (portConfig.mode === 'private' && Array.isArray(portConfig.whitelist)) {
368
+ if (!portConfig.whitelist.includes(deviceId)) {
369
+ logger.warn(`Device ${deviceId} is not whitelisted for port ${port}. Sending portclose.`);
370
+ this.rpc.portClose(ref);
371
+ this.connection.deleteConnection(ref);
372
+ return;
373
+ }
374
+ }
258
375
 
259
376
  if (protocol === 'udp') {
260
377
  // Send data to the local UDP service
package/utils.js CHANGED
@@ -80,19 +80,32 @@ function parseReason(reasonRaw) {
80
80
  }
81
81
  }
82
82
 
83
- function generateCert(path) {
84
- var kp = KEYUTIL.generateKeypair("EC", "secp256k1");
85
-
86
- var priv = KEYUTIL.getPEM(kp.prvKeyObj, "PKCS8PRV");
87
-
88
- pub = KEYUTIL.getPEM(kp.pubKeyObj, "PKCS8PUB");
89
-
83
+ function generateCert(privateKeyObj, publicKeyObj) {
84
+ // Generate a certificate valid for 1 month
85
+ function formatDate(date) {
86
+ const pad = n => n < 10 ? '0' + n : n;
87
+ return String(date.getUTCFullYear()).slice(2) +
88
+ pad(date.getUTCMonth() + 1) +
89
+ pad(date.getUTCDate()) +
90
+ pad(date.getUTCHours()) +
91
+ pad(date.getUTCMinutes()) +
92
+ pad(date.getUTCSeconds()) +
93
+ 'Z';
94
+ }
95
+
96
+ const now = new Date();
97
+ const notBefore = formatDate(new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000)); // 30 days before now
98
+ const notAfter = formatDate(new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000)); // 30 days after now
99
+
100
+
90
101
  var x = new KJUR.asn1.x509.Certificate({
91
102
  version: 3,
92
- serial: { int: 4 },
103
+ serial: { int: Math.floor(Math.random() * 1000000) },
93
104
  issuer: { str: "/CN=device" },
105
+ notbefore: notBefore,
106
+ notafter: notAfter,
94
107
  subject: { str: "/CN=device" },
95
- sbjpubkey: kp.pubKeyObj,
108
+ sbjpubkey: publicKeyObj,
96
109
  ext: [
97
110
  { extname: "basicConstraints", cA: false },
98
111
  { extname: "keyUsage", critical: true, names: ["digitalSignature"] },
@@ -102,14 +115,52 @@ function generateCert(path) {
102
115
  }
103
116
  ],
104
117
  sigalg: "SHA256withECDSA",
105
- cakey: kp.prvKeyObj
118
+ cakey: privateKeyObj
106
119
  });
107
120
 
121
+ // Get PEM representations
122
+ const priv = KEYUTIL.getPEM(privateKeyObj, "PKCS8PRV");
123
+
124
+
125
+ // Return the certificate with private key
126
+ return priv + x.getPEM();
127
+ }
108
128
 
109
- const pemFile = priv + x.getPEM();
110
- ensureDirectoryExistence(path);
111
-
112
- fs.writeFileSync(path, pemFile , 'utf8');
129
+ function loadOrGenerateKeyPair(keyLocation) {
130
+ try {
131
+ ensureDirectoryExistence(keyLocation);
132
+
133
+ // Try to load existing keys
134
+ if (fs.existsSync(keyLocation)) {
135
+ logger.info(`Loading keys from ${keyLocation}`);
136
+ const keyData = JSON.parse(fs.readFileSync(keyLocation, 'utf8'));
137
+
138
+ // Convert the stored JSON back to keypair objects
139
+ const prvKeyObj = KEYUTIL.getKeyFromPlainPrivatePKCS8PEM(keyData.privateKey);
140
+ const pubKeyObj = KEYUTIL.getKey(keyData.publicKey);
141
+
142
+ return { prvKeyObj, pubKeyObj };
143
+ } else {
144
+ // Generate new keypair
145
+ logger.info(`Generating new key pair at ${keyLocation}`);
146
+ const kp = KEYUTIL.generateKeypair("EC", "secp256k1");
147
+
148
+ // Store the keys in a serializable format
149
+ const keyData = {
150
+ privateKey: KEYUTIL.getPEM(kp.prvKeyObj, "PKCS8PRV"),
151
+ publicKey: KEYUTIL.getPEM(kp.pubKeyObj, "PKCS8PUB"),
152
+ check: kp.prvKeyObj.prvKeyHex
153
+ };
154
+
155
+ // Save to file
156
+ fs.writeFileSync(keyLocation, JSON.stringify(keyData, null, 2), 'utf8');
157
+
158
+ return kp;
159
+ }
160
+ } catch (error) {
161
+ logger.error(`Error loading or generating key pair: ${error}`);
162
+ throw error;
163
+ }
113
164
  }
114
165
 
115
166
  function ensureDirectoryExistence(filePath) {
@@ -121,4 +172,12 @@ function ensureDirectoryExistence(filePath) {
121
172
  fs.mkdirSync(dirname);
122
173
  }
123
174
 
124
- module.exports = { makeReadable, parseRequestId, parseResponseType, parseReason, generateCert };
175
+ module.exports = {
176
+ makeReadable,
177
+ parseRequestId,
178
+ parseResponseType,
179
+ parseReason,
180
+ generateCert,
181
+ loadOrGenerateKeyPair,
182
+ ensureDirectoryExistence
183
+ };