diodejs 0.2.2 → 0.4.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/bindPort.js CHANGED
@@ -2,14 +2,16 @@ const net = require('net');
2
2
  const tls = require('tls');
3
3
  const dgram = require('dgram');
4
4
  const { Buffer } = require('buffer');
5
+ const { toBufferView } = require('./utils');
5
6
  const { Duplex } = require('stream');
6
7
  const DiodeRPC = require('./rpc');
8
+ const nativeCrypto = require('./nativeCrypto');
7
9
  const logger = require('./logger');
8
10
 
9
11
  // Custom Duplex stream to handle the Diode connection
10
12
  class DiodeSocket extends Duplex {
11
13
  constructor(ref, rpc) {
12
- super();
14
+ super({ readableHighWaterMark: 256 * 1024, writableHighWaterMark: 256 * 1024, allowHalfOpen: false });
13
15
  this.ref = ref;
14
16
  this.rpc = rpc;
15
17
  this.destroyed = false;
@@ -50,7 +52,8 @@ class BindPort {
50
52
  [localPortOrPortsConfig]: {
51
53
  targetPort,
52
54
  deviceIdHex: this._stripHexPrefix(deviceIdHex),
53
- protocol: 'tls' // Default protocol is tls
55
+ protocol: 'tls', // Default protocol is tls
56
+ transport: 'api'
54
57
  }
55
58
  };
56
59
  } else {
@@ -67,13 +70,21 @@ class BindPort {
67
70
  if (!this.portsConfig[port].protocol) {
68
71
  this.portsConfig[port].protocol = 'tls';
69
72
  }
70
- // Ensure protocol is uppercase
73
+ // Ensure protocol is lowercase
71
74
  this.portsConfig[port].protocol = this.portsConfig[port].protocol.toLowerCase();
75
+
76
+ // Normalize transport (api or native)
77
+ const transportValue = this.portsConfig[port].transport !== undefined
78
+ ? this.portsConfig[port].transport
79
+ : this.portsConfig[port].native;
80
+ this.portsConfig[port].transport = this._normalizeTransport(transportValue);
72
81
  }
73
82
  }
74
83
 
75
84
  this.servers = new Map(); // Track server instances by localPort
76
- this.rpc = new DiodeRPC(this.connection);
85
+ this._rpcByConnection = new Map();
86
+ this.rpc = this._isManager() ? null : this._getRpcFor(this.connection);
87
+ this.handshakeTimeoutMs = parseInt(process.env.DIODE_NATIVE_HANDSHAKE_TIMEOUT_MS, 10) || 10000;
77
88
 
78
89
  // Set up listener for unsolicited messages once
79
90
  this._setupMessageListener();
@@ -86,23 +97,159 @@ class BindPort {
86
97
  }
87
98
  return hexString;
88
99
  }
100
+
101
+ _normalizeTransport(value) {
102
+ if (value === true) return 'native';
103
+ if (typeof value === 'string') {
104
+ const lower = value.toLowerCase();
105
+ if (lower === 'native') return 'native';
106
+ if (lower === 'api') return 'api';
107
+ }
108
+ return 'api';
109
+ }
110
+
111
+ _isManager() {
112
+ return this.connection && typeof this.connection.getConnectionForDevice === 'function';
113
+ }
114
+
115
+ _getRpcFor(connection) {
116
+ if (!connection) return null;
117
+ let rpc = this._rpcByConnection.get(connection);
118
+ if (!rpc) {
119
+ rpc = connection.RPC || new DiodeRPC(connection);
120
+ this._rpcByConnection.set(connection, rpc);
121
+ }
122
+ return rpc;
123
+ }
124
+
125
+ async _resolveConnectionForDevice(deviceId) {
126
+ if (this._isManager()) {
127
+ return this.connection.getConnectionForDevice(deviceId);
128
+ }
129
+ return this.connection;
130
+ }
131
+
132
+ async _openTlsHandshakeChannel(connection, rpc, ref) {
133
+ const diodeSocket = new DiodeSocket(ref, rpc);
134
+ const certPem = connection.getDeviceCertificate();
135
+ if (!certPem) {
136
+ throw new Error('No device certificate available');
137
+ }
138
+
139
+ const tlsOptions = {
140
+ cert: certPem,
141
+ key: certPem,
142
+ rejectUnauthorized: false,
143
+ ciphers: 'ECDHE-ECDSA-AES256-GCM-SHA384',
144
+ ecdhCurve: 'secp256k1',
145
+ minVersion: 'TLSv1.2',
146
+ maxVersion: 'TLSv1.2',
147
+ };
148
+
149
+ const tlsSocket = tls.connect({
150
+ socket: diodeSocket,
151
+ ...tlsOptions
152
+ });
153
+ tlsSocket.setNoDelay(true);
154
+
155
+ const socketWrapper = {
156
+ diodeSocket,
157
+ tlsSocket,
158
+ end: () => {
159
+ try { tlsSocket.end(); } catch {}
160
+ try { diodeSocket._destroy(null, () => {}); } catch {}
161
+ }
162
+ };
163
+
164
+ connection.addClientSocket(ref, socketWrapper);
165
+
166
+ await new Promise((resolve, reject) => {
167
+ const timer = setTimeout(() => reject(new Error('TLS handshake timeout')), this.handshakeTimeoutMs);
168
+ tlsSocket.once('secureConnect', () => {
169
+ clearTimeout(timer);
170
+ resolve();
171
+ });
172
+ tlsSocket.once('error', (err) => {
173
+ clearTimeout(timer);
174
+ reject(err);
175
+ });
176
+ });
177
+
178
+ return { tlsSocket, socketWrapper };
179
+ }
180
+
181
+ async _performNativeHandshake(connection, rpc, deviceId, targetPort, physicalPort) {
182
+ const handshakePort = `tls:${targetPort}#hs`;
183
+ const ref = await rpc.portOpen(deviceId, handshakePort, 'rw');
184
+ if (!ref) {
185
+ throw new Error('Handshake portopen failed');
186
+ }
187
+
188
+ let tlsSocket;
189
+ try {
190
+ ({ tlsSocket } = await this._openTlsHandshakeChannel(connection, rpc, ref));
191
+
192
+ const localDeviceId = connection.getEthereumAddress().toLowerCase();
193
+ const remoteDeviceId = `0x${Buffer.from(deviceId).toString('hex')}`.toLowerCase();
194
+ const { message, privKey, nonce } = nativeCrypto.createHandshakeMessage({
195
+ role: 'bind',
196
+ deviceId: localDeviceId,
197
+ physicalPort,
198
+ privateKey: connection.getPrivateKey()
199
+ });
200
+
201
+ nativeCrypto.writeHandshakeMessage(tlsSocket, message);
202
+
203
+ const peerMessage = await nativeCrypto.readHandshakeMessage(tlsSocket, this.handshakeTimeoutMs);
204
+ const verification = nativeCrypto.verifyHandshakeMessage(peerMessage, {
205
+ expectedRole: 'publish',
206
+ expectedDeviceId: remoteDeviceId,
207
+ expectedPhysicalPort: physicalPort
208
+ });
209
+ if (!verification.ok) {
210
+ throw new Error(`Handshake verification failed: ${verification.reason}`);
211
+ }
212
+
213
+ const session = nativeCrypto.deriveSessionKeys({
214
+ role: 'bind',
215
+ localDeviceId,
216
+ remoteDeviceId,
217
+ localEphPriv: privKey,
218
+ remoteEphPub: verification.ephPub,
219
+ localNonce: nonce,
220
+ remoteNonce: verification.nonce,
221
+ physicalPort,
222
+ });
223
+
224
+ return session;
225
+ } finally {
226
+ try { if (tlsSocket) tlsSocket.end(); } catch {}
227
+ try { await rpc.portClose(ref); } catch {}
228
+ try { connection.deleteClientSocket(ref); } catch {}
229
+ }
230
+ }
89
231
 
90
232
  _setupMessageListener() {
91
233
  // Listen for data events from the device
92
- this.connection.on('unsolicited', (message) => {
234
+ this.connection.on('unsolicited', (message, sourceConnection) => {
235
+ const connection = sourceConnection || this.connection;
236
+ if (!connection || typeof connection.getClientSocket !== 'function') {
237
+ logger.warn(() => 'Received unsolicited message without a valid connection context');
238
+ return;
239
+ }
93
240
  const [messageIdRaw, messageContent] = message;
94
241
  const messageTypeRaw = messageContent[0];
95
- const messageType = Buffer.from(messageTypeRaw).toString('utf8');
242
+ const messageType = toBufferView(messageTypeRaw).toString('utf8');
96
243
 
97
244
  if (messageType === 'data' || messageType === 'portsend') {
98
245
  const refRaw = messageContent[1];
99
246
  const dataRaw = messageContent[2];
100
247
 
101
- const dataRef = Buffer.from(refRaw);
102
- const data = Buffer.from(dataRaw);
248
+ const dataRef = toBufferView(refRaw);
249
+ const data = toBufferView(dataRaw);
103
250
 
104
251
  // Find the associated client socket from connection
105
- const clientSocket = this.connection.getClientSocket(dataRef);
252
+ const clientSocket = connection.getClientSocket(dataRef);
106
253
  if (clientSocket) {
107
254
  if (clientSocket.diodeSocket) {
108
255
  // If it's a DiodeSocket, push data to it so tls can process
@@ -112,57 +259,58 @@ class BindPort {
112
259
  clientSocket.write(data);
113
260
  }
114
261
  } else {
115
- const connectionInfo = this.connection.getConnection(dataRef);
262
+ const connectionInfo = connection.getConnection(dataRef);
116
263
  if (connectionInfo) {
117
- logger.debug(`No client socket found for ref: ${dataRef.toString('hex')}, but connection exists for ${connectionInfo.host}:${connectionInfo.port}`);
264
+ logger.debug(() => `No client socket found for ref: ${dataRef.toString('hex')}, but connection exists for ${connectionInfo.host}:${connectionInfo.port}`);
118
265
  } else {
119
- logger.warn(`No client socket found for ref: ${dataRef.toString('hex')}`);
266
+ logger.warn(() => `No client socket found for ref: ${dataRef.toString('hex')}`);
120
267
  }
121
268
  }
122
269
  } else if (messageType === 'portclose') {
123
270
  const refRaw = messageContent[1];
124
- const dataRef = Buffer.from(refRaw);
271
+ const dataRef = toBufferView(refRaw);
125
272
 
126
273
  // Close the associated client socket
127
- const clientSocket = this.connection.getClientSocket(dataRef);
274
+ const clientSocket = connection.getClientSocket(dataRef);
128
275
  if (clientSocket) {
129
276
  if (clientSocket.diodeSocket) {
130
277
  clientSocket.diodeSocket._destroy(null, () => {});
131
278
  }
132
279
  clientSocket.end();
133
- this.connection.deleteClientSocket(dataRef);
134
- logger.info(`Port closed for ref: ${dataRef.toString('hex')}`);
280
+ connection.deleteClientSocket(dataRef);
281
+ logger.info(() => `Port closed for ref: ${dataRef.toString('hex')}`);
135
282
  }
136
283
  } else {
137
- if (messageType != 'portopen') {
138
- logger.warn(`Unknown unsolicited message type: ${messageType}`);
284
+ if (messageType != 'portopen' && messageType != 'portopen2' && messageType != 'ticket_request') {
285
+ logger.warn(() => `Unknown unsolicited message type: ${messageType}`);
139
286
  }
140
287
  }
141
288
  });
142
289
 
143
290
  // Handle device disconnect
144
291
  this.connection.on('end', () => {
145
- logger.info('Disconnected from Diode.io server');
292
+ logger.info(() => 'Disconnected from Diode.io server');
146
293
  this.closeAllServers();
147
294
  });
148
295
 
149
296
  // Handle connection errors
150
297
  this.connection.on('error', (err) => {
151
- logger.error(`Connection error: ${err}`);
298
+ logger.error(() => `Connection error: ${err}`);
152
299
  this.closeAllServers();
153
300
  });
154
301
  }
155
302
 
156
- addPort(localPort, targetPort, deviceIdHex, protocol = 'tls') {
303
+ addPort(localPort, targetPort, deviceIdHex, protocol = 'tls', transport = undefined) {
157
304
  if (this.servers.has(localPort)) {
158
- logger.warn(`Port ${localPort} is already bound`);
305
+ logger.warn(() => `Port ${localPort} is already bound`);
159
306
  return false;
160
307
  }
161
308
 
162
309
  this.portsConfig[localPort] = {
163
310
  targetPort,
164
311
  deviceIdHex: this._stripHexPrefix(deviceIdHex),
165
- protocol: protocol.toLowerCase()
312
+ protocol: protocol.toLowerCase(),
313
+ transport: this._normalizeTransport(transport)
166
314
  };
167
315
 
168
316
  this.bindSinglePort(localPort);
@@ -172,7 +320,7 @@ class BindPort {
172
320
 
173
321
  removePort(localPort) {
174
322
  if (!this.portsConfig[localPort]) {
175
- logger.warn(`Port ${localPort} is not configured`);
323
+ logger.warn(() => `Port ${localPort} is not configured`);
176
324
  return false;
177
325
  }
178
326
 
@@ -180,7 +328,7 @@ class BindPort {
180
328
  if (this.servers.has(localPort)) {
181
329
  const server = this.servers.get(localPort);
182
330
  server.close(() => {
183
- logger.info(`Server on port ${localPort} closed`);
331
+ logger.info(() => `Server on port ${localPort} closed`);
184
332
  });
185
333
  this.servers.delete(localPort);
186
334
  }
@@ -193,7 +341,7 @@ class BindPort {
193
341
  closeAllServers() {
194
342
  for (const [localPort, server] of this.servers.entries()) {
195
343
  server.close();
196
- logger.info(`Server on port ${localPort} closed`);
344
+ logger.info(() => `Server on port ${localPort} closed`);
197
345
  }
198
346
  this.servers.clear();
199
347
  }
@@ -201,43 +349,153 @@ class BindPort {
201
349
  bindSinglePort(localPort) {
202
350
  const config = this.portsConfig[localPort];
203
351
  if (!config) {
204
- logger.error(`No configuration found for port ${localPort}`);
352
+ logger.error(() => `No configuration found for port ${localPort}`);
205
353
  return false;
206
354
  }
207
355
 
208
356
  const { targetPort, deviceIdHex, protocol = 'tls' } = config;
357
+ const transport = config.transport || 'api';
358
+ const useNative = transport === 'native' && (protocol === 'tcp' || protocol === 'udp');
359
+ if (transport === 'native' && protocol === 'tls') {
360
+ logger.warn(() => `Native transport does not support TLS for port ${localPort}. Falling back to API relay.`);
361
+ }
209
362
  const deviceId = Buffer.from(deviceIdHex, 'hex');
210
363
 
211
364
  // Format the target port with protocol prefix for the remote connection
212
365
  const formattedTargetPort = `${protocol}:${targetPort}`;
213
- logger.info(`Binding local port ${localPort} to remote ${formattedTargetPort}`);
366
+ logger.info(() => `Binding local port ${localPort} to remote ${formattedTargetPort}`);
214
367
 
215
368
  // For udp protocol, use udp server
216
369
  if (protocol === 'udp') {
217
370
  const server = dgram.createSocket('udp4');
218
371
 
219
372
  server.on('listening', () => {
220
- logger.info(`udp server listening on port ${localPort} forwarding to device port ${targetPort}`);
373
+ logger.info(() => `udp server listening on port ${localPort} forwarding to device port ${targetPort}`);
221
374
  });
222
375
 
223
376
  server.on('message', async (data, rinfo) => {
224
- // Open a new port on the device if this is a new client
225
377
  const clientKey = `${rinfo.address}:${rinfo.port}`;
226
- let ref = server.clientRefs && server.clientRefs[clientKey];
378
+ if (useNative) {
379
+ if (!server.nativeRelays) server.nativeRelays = {};
380
+ let relayInfo = server.nativeRelays[clientKey];
381
+ if (!relayInfo) {
382
+ let connection;
383
+ try {
384
+ connection = await this._resolveConnectionForDevice(deviceId);
385
+ } catch (error) {
386
+ logger.error(() => `Error resolving relay for device ${deviceIdHex}: ${error}`);
387
+ return;
388
+ }
389
+ if (!connection) {
390
+ logger.error(() => `No relay connection available for device ${deviceIdHex}`);
391
+ return;
392
+ }
393
+ const rpc = this._getRpcFor(connection);
394
+ try {
395
+ const flags = config.flags || 'rwu';
396
+ const physicalPort = await rpc.portOpen2(deviceId, formattedTargetPort, flags);
397
+ if (!physicalPort) {
398
+ logger.error(() => `Error opening portopen2 ${formattedTargetPort} on deviceId: ${deviceIdHex}`);
399
+ return;
400
+ }
401
+
402
+ const relaySocket = dgram.createSocket('udp4');
403
+ relaySocket.on('message', (msg) => {
404
+ if (!relayInfo || !relayInfo.session) return;
405
+ const plaintext = nativeCrypto.parseUdpPacket(relayInfo.session, msg);
406
+ if (!plaintext) return;
407
+ server.send(plaintext, rinfo.port, rinfo.address);
408
+ });
409
+ relaySocket.on('error', (err) => {
410
+ logger.error(() => `udp relay socket error: ${err}`);
411
+ try { relaySocket.close(); } catch {}
412
+ delete server.nativeRelays[clientKey];
413
+ });
414
+ relaySocket.bind(0);
415
+
416
+ relayInfo = {
417
+ socket: relaySocket,
418
+ physicalPort,
419
+ relayHost: connection.getServerRelayHost(),
420
+ connection,
421
+ session: null,
422
+ handshakePromise: null,
423
+ client: { address: rinfo.address, port: rinfo.port }
424
+ };
425
+ server.nativeRelays[clientKey] = relayInfo;
426
+ logger.info(() => `Portopen2 ${formattedTargetPort} opened with server port ${physicalPort} for udp client ${clientKey}`);
427
+ } catch (error) {
428
+ logger.error(() => `Error opening portopen2 ${formattedTargetPort} on device: ${error}`);
429
+ return;
430
+ }
431
+ }
432
+
433
+ if (!relayInfo.handshakePromise) {
434
+ const rpc = this._getRpcFor(relayInfo.connection);
435
+ relayInfo.handshakePromise = this._performNativeHandshake(
436
+ relayInfo.connection,
437
+ rpc,
438
+ deviceId,
439
+ targetPort,
440
+ relayInfo.physicalPort
441
+ ).then((session) => {
442
+ relayInfo.session = session;
443
+ return session;
444
+ }).catch((error) => {
445
+ logger.error(() => `Native UDP handshake failed: ${error}`);
446
+ try { relayInfo.socket.close(); } catch {}
447
+ delete server.nativeRelays[clientKey];
448
+ throw error;
449
+ });
450
+ }
451
+
452
+ try {
453
+ await relayInfo.handshakePromise;
454
+ } catch (_) {
455
+ return;
456
+ }
457
+
458
+ if (!relayInfo.session) return;
459
+
460
+ // Send encrypted data to the server relay port
461
+ try {
462
+ const packet = nativeCrypto.createUdpPacket(relayInfo.session, data);
463
+ relayInfo.socket.send(packet, relayInfo.physicalPort, relayInfo.relayHost);
464
+ } catch (error) {
465
+ logger.error(() => `Error sending udp data to relay: ${error}`);
466
+ }
467
+ return;
468
+ }
469
+
470
+ // Legacy API relay
471
+ let entry = server.clientRefs && server.clientRefs[clientKey];
227
472
 
228
- if (!ref) {
473
+ if (!entry) {
474
+ let connection;
229
475
  try {
230
- ref = await this.rpc.portOpen(deviceId, formattedTargetPort, 'rw');
476
+ connection = await this._resolveConnectionForDevice(deviceId);
477
+ } catch (error) {
478
+ logger.error(() => `Error resolving relay for device ${deviceIdHex}: ${error}`);
479
+ return;
480
+ }
481
+ if (!connection) {
482
+ logger.error(() => `No relay connection available for device ${deviceIdHex}`);
483
+ return;
484
+ }
485
+ const rpc = this._getRpcFor(connection);
486
+ try {
487
+ const ref = await rpc.portOpen(deviceId, formattedTargetPort, 'rw');
231
488
  if (!ref) {
232
- logger.error(`Error opening port ${formattedTargetPort} on deviceId: ${deviceIdHex}`);
489
+ logger.error(() => `Error opening port ${formattedTargetPort} on deviceId: ${deviceIdHex}`);
233
490
  return;
234
491
  } else {
235
- logger.info(`Port ${formattedTargetPort} opened on device with ref: ${ref.toString('hex')} for udp client ${clientKey}`);
492
+ logger.info(() => `Port ${formattedTargetPort} opened on device with ref: ${ref.toString('hex')} for udp client ${clientKey}`);
236
493
  if (!server.clientRefs) server.clientRefs = {};
237
- server.clientRefs[clientKey] = ref;
494
+ entry = { ref, connection };
495
+ server.clientRefs[clientKey] = entry;
238
496
 
239
497
  // Store the client info
240
- this.connection.addClientSocket(ref, {
498
+ connection.addClientSocket(ref, {
241
499
  address: rinfo.address,
242
500
  port: rinfo.port,
243
501
  protocol: 'udp',
@@ -247,43 +505,164 @@ class BindPort {
247
505
  });
248
506
  }
249
507
  } catch (error) {
250
- logger.error(`Error opening port ${formattedTargetPort} on device: ${error}`);
508
+ logger.error(() => `Error opening port ${formattedTargetPort} on device: ${error}`);
251
509
  return;
252
510
  }
253
511
  }
254
512
 
255
513
  // Send data to the device
256
514
  try {
257
- await this.rpc.portSend(ref, data);
515
+ const rpc = this._getRpcFor(entry.connection);
516
+ await rpc.portSend(entry.ref, data);
258
517
  } catch (error) {
259
- logger.error(`Error sending udp data to device: ${error}`);
518
+ logger.error(() => `Error sending udp data to device: ${error}`);
260
519
  }
261
520
  });
262
521
 
263
522
  server.on('error', (err) => {
264
- logger.error(`udp Server error: ${err}`);
523
+ logger.error(() => `udp Server error: ${err}`);
265
524
  });
266
525
 
526
+ server.on('close', () => {
527
+ if (server.nativeRelays) {
528
+ for (const relayInfo of Object.values(server.nativeRelays)) {
529
+ try { relayInfo.socket.close(); } catch {}
530
+ }
531
+ server.nativeRelays = null;
532
+ }
533
+ });
534
+
267
535
  server.bind(localPort);
268
536
  this.servers.set(parseInt(localPort), server);
269
537
  } else {
270
538
  // For TCP and tls protocols, use TCP server locally
271
539
  const server = net.createServer(async (clientSocket) => {
272
- logger.info(`Client connected to local server on port ${localPort}`);
540
+ logger.info(() => `Client connected to local server on port ${localPort}`);
541
+ clientSocket.setNoDelay(true);
542
+
543
+ let connection;
544
+ try {
545
+ connection = await this._resolveConnectionForDevice(deviceId);
546
+ } catch (error) {
547
+ logger.error(() => `Error resolving relay for device ${deviceIdHex}: ${error}`);
548
+ clientSocket.destroy();
549
+ return;
550
+ }
551
+ if (!connection) {
552
+ logger.error(() => `No relay connection available for device ${deviceIdHex}`);
553
+ clientSocket.destroy();
554
+ return;
555
+ }
556
+ const rpc = this._getRpcFor(connection);
273
557
 
274
- // Open a new port on the device for this client
275
558
  let ref;
559
+ if (useNative) {
560
+ // Open a new native relay port on the device for this client
561
+ let physicalPort;
562
+ try {
563
+ const flags = config.flags || 'rw';
564
+ physicalPort = await rpc.portOpen2(deviceId, formattedTargetPort, flags);
565
+ if (!physicalPort) {
566
+ logger.error(() => `Error opening portopen2 ${formattedTargetPort} on deviceId: ${deviceIdHex}`);
567
+ clientSocket.destroy();
568
+ return;
569
+ }
570
+ } catch (error) {
571
+ logger.error(() => `Error opening portopen2 ${formattedTargetPort} on device: ${error}`);
572
+ clientSocket.destroy();
573
+ return;
574
+ }
575
+
576
+ let session;
577
+ try {
578
+ session = await this._performNativeHandshake(connection, rpc, deviceId, targetPort, physicalPort);
579
+ } catch (error) {
580
+ logger.error(() => `Native TCP handshake failed: ${error}`);
581
+ clientSocket.destroy();
582
+ return;
583
+ }
584
+
585
+ const relayHost = connection.getServerRelayHost();
586
+ const relaySocket = net.connect({ host: relayHost, port: physicalPort }, () => {
587
+ logger.info(() => `Connected to relay ${relayHost}:${physicalPort} for ${formattedTargetPort}`);
588
+ });
589
+ relaySocket.setNoDelay(true);
590
+
591
+ let relayReady = false;
592
+ const pendingChunks = [];
593
+
594
+ const cleanup = () => {
595
+ if (!clientSocket.destroyed) clientSocket.destroy();
596
+ if (!relaySocket.destroyed) relaySocket.destroy();
597
+ };
598
+
599
+ relaySocket.on('connect', () => {
600
+ relayReady = true;
601
+ while (pendingChunks.length > 0) {
602
+ const chunk = pendingChunks.shift();
603
+ try {
604
+ const frame = nativeCrypto.createTcpFrame(session, chunk);
605
+ relaySocket.write(frame);
606
+ } catch (error) {
607
+ logger.error(() => `Error sending TCP frame: ${error}`);
608
+ cleanup();
609
+ break;
610
+ }
611
+ }
612
+ });
613
+
614
+ relaySocket.on('data', (data) => {
615
+ try {
616
+ const messages = nativeCrypto.consumeTcpFrames(session, data);
617
+ for (const msg of messages) {
618
+ clientSocket.write(msg);
619
+ }
620
+ } catch (error) {
621
+ logger.error(() => `TCP decrypt error: ${error}`);
622
+ cleanup();
623
+ }
624
+ });
625
+
626
+ clientSocket.on('data', (data) => {
627
+ if (!relayReady) {
628
+ pendingChunks.push(data);
629
+ return;
630
+ }
631
+ try {
632
+ const frame = nativeCrypto.createTcpFrame(session, data);
633
+ relaySocket.write(frame);
634
+ } catch (error) {
635
+ logger.error(() => `Error sending TCP frame: ${error}`);
636
+ cleanup();
637
+ }
638
+ });
639
+
640
+ relaySocket.on('error', (err) => {
641
+ logger.error(() => `Relay socket error: ${err}`);
642
+ cleanup();
643
+ });
644
+ clientSocket.on('error', (err) => {
645
+ logger.error(() => `Client socket error: ${err}`);
646
+ cleanup();
647
+ });
648
+ clientSocket.on('end', cleanup);
649
+ relaySocket.on('end', cleanup);
650
+
651
+ return;
652
+ }
653
+
654
+ // Legacy API relay
276
655
  try {
277
- ref = await this.rpc.portOpen(deviceId, formattedTargetPort, 'rw');
656
+ ref = await rpc.portOpen(deviceId, formattedTargetPort, 'rw');
278
657
  if (!ref) {
279
- logger.error(`Error opening port ${formattedTargetPort} on deviceId: ${deviceIdHex}`);
658
+ logger.error(() => `Error opening port ${formattedTargetPort} on deviceId: ${deviceIdHex}`);
280
659
  clientSocket.destroy();
281
660
  return;
282
661
  } else {
283
- logger.info(`Port ${formattedTargetPort} opened on device with ref: ${ref.toString('hex')} for client`);
662
+ logger.info(() => `Port ${formattedTargetPort} opened on device with ref: ${ref.toString('hex')} for client`);
284
663
  }
285
664
  } catch (error) {
286
- logger.error(`Error opening port ${formattedTargetPort} on device: ${error}`);
665
+ logger.error(() => `Error opening port ${formattedTargetPort} on device: ${error}`);
287
666
  clientSocket.destroy();
288
667
  return;
289
668
  }
@@ -292,10 +671,10 @@ class BindPort {
292
671
  // For tls protocol, create a proper tls connection
293
672
  try {
294
673
  // Create a DiodeSocket to handle communication with the device
295
- const diodeSocket = new DiodeSocket(ref, this.rpc);
674
+ const diodeSocket = new DiodeSocket(ref, rpc);
296
675
 
297
676
  // Get the device certificate for tls
298
- const certPem = this.connection.getDeviceCertificate();
677
+ const certPem = connection.getDeviceCertificate();
299
678
  if (!certPem) {
300
679
  throw new Error('No device certificate available');
301
680
  }
@@ -316,15 +695,16 @@ class BindPort {
316
695
  socket: diodeSocket,
317
696
  ...tlsOptions
318
697
  }, () => {
319
- logger.info(`tls connection established to device ${deviceIdHex}`);
698
+ logger.info(() => `tls connection established to device ${deviceIdHex}`);
320
699
  });
700
+ tlsSocket.setNoDelay(true);
321
701
 
322
702
  // Pipe data between the client socket and the tls socket
323
703
  tlsSocket.pipe(clientSocket).pipe(tlsSocket);
324
704
 
325
705
  // Handle tls socket errors
326
706
  tlsSocket.on('error', (err) => {
327
- logger.error(`tls Socket error: ${err}`);
707
+ logger.error(() => `tls Socket error: ${err}`);
328
708
  clientSocket.destroy();
329
709
  });
330
710
 
@@ -339,23 +719,23 @@ class BindPort {
339
719
  };
340
720
 
341
721
  // Store the socket wrapper
342
- this.connection.addClientSocket(ref, socketWrapper);
722
+ connection.addClientSocket(ref, socketWrapper);
343
723
 
344
724
  } catch (error) {
345
- logger.error(`Error setting up tls connection: ${error}`);
725
+ logger.error(() => `Error setting up tls connection: ${error}`);
346
726
  clientSocket.destroy();
347
727
  return;
348
728
  }
349
729
  } else {
350
730
  // For TCP protocol, just use the raw socket
351
- this.connection.addClientSocket(ref, clientSocket);
731
+ connection.addClientSocket(ref, clientSocket);
352
732
 
353
733
  // Handle data from client to device
354
734
  clientSocket.on('data', async (data) => {
355
735
  try {
356
- await this.rpc.portSend(ref, data);
736
+ await rpc.portSend(ref, data);
357
737
  } catch (error) {
358
- logger.error(`Error sending data to device: ${error}`);
738
+ logger.error(() => `Error sending data to device: ${error}`);
359
739
  clientSocket.destroy();
360
740
  }
361
741
  });
@@ -363,28 +743,28 @@ class BindPort {
363
743
 
364
744
  // Handle client socket closure (common for all protocols)
365
745
  clientSocket.on('end', async () => {
366
- logger.info('Client disconnected');
367
- if (ref && this.connection.hasClientSocket(ref)) {
746
+ logger.info(() => 'Client disconnected');
747
+ if (ref && connection.hasClientSocket(ref)) {
368
748
  try {
369
- await this.rpc.portClose(ref);
370
- logger.info(`Port closed on device for ref: ${ref.toString('hex')}`);
371
- this.connection.deleteClientSocket(ref);
749
+ await rpc.portClose(ref);
750
+ logger.info(() => `Port closed on device for ref: ${ref.toString('hex')}`);
751
+ connection.deleteClientSocket(ref);
372
752
  } catch (error) {
373
- logger.error(`Error closing port on device: ${error}`);
753
+ logger.error(() => `Error closing port on device: ${error}`);
374
754
  }
375
755
  } else {
376
- logger.warn('Ref is invalid or no longer in clientSockets.');
756
+ logger.warn(() => 'Ref is invalid or no longer in clientSockets.');
377
757
  }
378
758
  });
379
759
 
380
760
  // Handle client socket errors
381
761
  clientSocket.on('error', (err) => {
382
- logger.error(`Client socket error: ${err}`);
762
+ logger.error(() => `Client socket error: ${err}`);
383
763
  });
384
764
  });
385
765
 
386
766
  server.listen(localPort, () => {
387
- logger.info(`Local server listening on port ${localPort} forwarding to device ${protocol} port ${targetPort}`);
767
+ logger.info(() => `Local server listening on port ${localPort} forwarding to device ${protocol} port ${targetPort}`);
388
768
  });
389
769
 
390
770
  this.servers.set(parseInt(localPort), server);
@@ -404,4 +784,4 @@ class BindPort {
404
784
  }
405
785
  }
406
786
 
407
- module.exports = BindPort;
787
+ module.exports = BindPort;