node-rtc-connection 1.0.18 → 1.0.19
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/dist/index.cjs +200 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +200 -15
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/ice/RTCIceTransport.js +28 -8
- package/src/peerconnection/RTCPeerConnection.js +27 -3
- package/src/sdp/sdp-utils.js +1 -1
- package/src/stun/stun-client.js +135 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-rtc-connection",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.19",
|
|
4
4
|
"description": "WebRTC DataChannel implementation for Node.js with STUN, TURN, NAT traversal, and encryption. Pure Node.js, no native dependencies.",
|
|
5
5
|
"main": "dist/index.cjs",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -357,7 +357,10 @@ class RTCIceTransport extends EventEmitter {
|
|
|
357
357
|
|
|
358
358
|
// Validate that candidate has required properties for connectivity checks
|
|
359
359
|
if (!parsedCandidate.address || parsedCandidate.port === null || parsedCandidate.port === undefined) {
|
|
360
|
-
|
|
360
|
+
// Only warn if it's not an empty candidate (which signals end-of-candidates)
|
|
361
|
+
if (parsedCandidate.candidate !== '') {
|
|
362
|
+
console.warn('Remote candidate missing address or port, skipping connectivity checks');
|
|
363
|
+
}
|
|
361
364
|
// Store the original candidate for compatibility
|
|
362
365
|
this._remoteCandidates.push(candidate);
|
|
363
366
|
return;
|
|
@@ -684,6 +687,11 @@ class RTCIceTransport extends EventEmitter {
|
|
|
684
687
|
// Store TURN client as the "socket" for this relay candidate
|
|
685
688
|
this._sockets.set(foundation, { type: 'turn', client: turnClient });
|
|
686
689
|
|
|
690
|
+
// Handle incoming data from TURN
|
|
691
|
+
turnClient.on('data', (data, peer) => {
|
|
692
|
+
this._handleSocketMessage(data, peer, candidate);
|
|
693
|
+
});
|
|
694
|
+
|
|
687
695
|
this._addLocalCandidate(candidate);
|
|
688
696
|
|
|
689
697
|
// Keep allocation alive
|
|
@@ -760,8 +768,8 @@ class RTCIceTransport extends EventEmitter {
|
|
|
760
768
|
*/
|
|
761
769
|
_sendConnectivityCheck(pair) {
|
|
762
770
|
const socket = this._sockets.get(pair.local.foundation);
|
|
763
|
-
if (!socket
|
|
764
|
-
return;
|
|
771
|
+
if (!socket) {
|
|
772
|
+
return;
|
|
765
773
|
}
|
|
766
774
|
|
|
767
775
|
// Validate remote candidate has required properties
|
|
@@ -778,11 +786,23 @@ class RTCIceTransport extends EventEmitter {
|
|
|
778
786
|
const request = this._createBindingRequest(transactionId);
|
|
779
787
|
|
|
780
788
|
try {
|
|
781
|
-
socket.
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
789
|
+
if (socket.type === 'turn') {
|
|
790
|
+
const turnClient = socket.client;
|
|
791
|
+
// Create permission and send indication
|
|
792
|
+
turnClient.createPermission(pair.remote.address)
|
|
793
|
+
.then(() => {
|
|
794
|
+
return turnClient.sendIndication(pair.remote.address, pair.remote.port, request);
|
|
795
|
+
})
|
|
796
|
+
.catch(err => {
|
|
797
|
+
// Suppress errors for now as this happens frequently during connection
|
|
798
|
+
});
|
|
799
|
+
} else {
|
|
800
|
+
socket.send(request, pair.remote.port, pair.remote.address, (err) => {
|
|
801
|
+
if (err) {
|
|
802
|
+
console.error(`Connectivity check failed for ${pair.remote.address}:${pair.remote.port}:`, err);
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
}
|
|
786
806
|
} catch (err) {
|
|
787
807
|
console.error(`Error sending connectivity check to ${pair.remote.address || 'unknown'}:${pair.remote.port || 'unknown'}:`, err);
|
|
788
808
|
}
|
|
@@ -602,6 +602,11 @@ class RTCPeerConnection extends EventEmitter {
|
|
|
602
602
|
console.error('Failed to establish network connection:', error);
|
|
603
603
|
}
|
|
604
604
|
|
|
605
|
+
// Snapshot the candidates BEFORE starting ICE to avoid race condition
|
|
606
|
+
// If addIceCandidate runs while start() is yielding, it will see transport running and add the candidate.
|
|
607
|
+
// If we snapshot after start(), we might add the same candidate twice.
|
|
608
|
+
const candidatesToAdd = [...this._remoteIceCandidates];
|
|
609
|
+
|
|
605
610
|
// Start ICE
|
|
606
611
|
if (iceParams.usernameFragment && iceParams.password) {
|
|
607
612
|
try {
|
|
@@ -612,7 +617,7 @@ class RTCPeerConnection extends EventEmitter {
|
|
|
612
617
|
}
|
|
613
618
|
|
|
614
619
|
// Add remote candidates
|
|
615
|
-
for (const candidate of
|
|
620
|
+
for (const candidate of candidatesToAdd) {
|
|
616
621
|
try {
|
|
617
622
|
// Parse candidate string (simplified)
|
|
618
623
|
await this._iceTransport.addRemoteCandidate(candidate);
|
|
@@ -672,7 +677,7 @@ class RTCPeerConnection extends EventEmitter {
|
|
|
672
677
|
throw new Error('RTCPeerConnection is closed');
|
|
673
678
|
}
|
|
674
679
|
|
|
675
|
-
if (!candidate) {
|
|
680
|
+
if (!candidate || (candidate.candidate === '')) {
|
|
676
681
|
// End of candidates signal
|
|
677
682
|
this._iceGatheringState = RTCIceGatheringState.COMPLETE;
|
|
678
683
|
this.emit('icegatheringstatechange');
|
|
@@ -685,7 +690,26 @@ class RTCPeerConnection extends EventEmitter {
|
|
|
685
690
|
this._remoteIceCandidates.push(candidate);
|
|
686
691
|
|
|
687
692
|
// If connection is already started, add candidate immediately
|
|
688
|
-
if (
|
|
693
|
+
// Check if transport is actually started (not just initialized)
|
|
694
|
+
// We can infer this if we have remote description AND the transport state is not 'new'
|
|
695
|
+
// Or simply try/catch and ignore "not started" error, but better to check.
|
|
696
|
+
// Since we don't have public access to _started, we rely on state.
|
|
697
|
+
// However, state might be 'new' but start() was called? No, start() sets state to checking.
|
|
698
|
+
|
|
699
|
+
// Actually, _startConnection calls start().
|
|
700
|
+
// If we are Answerer, we set remote offer. _startConnection is NOT called yet.
|
|
701
|
+
// It is called when we set local answer.
|
|
702
|
+
// So we should NOT add candidates yet.
|
|
703
|
+
|
|
704
|
+
// If we are Offerer, we set remote answer. _startConnection IS called.
|
|
705
|
+
|
|
706
|
+
// So we should only add if we have both descriptions (Stable state)?
|
|
707
|
+
// Or if the transport is started.
|
|
708
|
+
|
|
709
|
+
// Let's check if we are in a state where transport should be running.
|
|
710
|
+
const isTransportRunning = this._iceTransport.state !== 'new' && this._iceTransport.state !== 'closed';
|
|
711
|
+
|
|
712
|
+
if (isTransportRunning) {
|
|
689
713
|
try {
|
|
690
714
|
await this._iceTransport.addRemoteCandidate(candidate);
|
|
691
715
|
} catch (error) {
|
package/src/sdp/sdp-utils.js
CHANGED
package/src/stun/stun-client.js
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
const dgram = require('dgram');
|
|
13
13
|
const crypto = require('crypto');
|
|
14
14
|
|
|
15
|
+
const EventEmitter = require('events');
|
|
16
|
+
|
|
15
17
|
/**
|
|
16
18
|
* STUN message types
|
|
17
19
|
*/
|
|
@@ -69,7 +71,7 @@ const MAGIC_COOKIE = 0x2112A442;
|
|
|
69
71
|
* @class STUNClient
|
|
70
72
|
* @description STUN/TURN client for NAT traversal
|
|
71
73
|
*/
|
|
72
|
-
class STUNClient {
|
|
74
|
+
class STUNClient extends EventEmitter {
|
|
73
75
|
/**
|
|
74
76
|
* Create a STUN client
|
|
75
77
|
* @param {Object} options - Client options
|
|
@@ -81,6 +83,7 @@ class STUNClient {
|
|
|
81
83
|
* @param {Object} [options.params={}] - Additional query parameters from URL
|
|
82
84
|
*/
|
|
83
85
|
constructor(options) {
|
|
86
|
+
super();
|
|
84
87
|
this.server = options.server;
|
|
85
88
|
this.port = options.port;
|
|
86
89
|
this.username = options.username;
|
|
@@ -204,6 +207,46 @@ class STUNClient {
|
|
|
204
207
|
return this._sendRequest(request, transactionId, 'refresh');
|
|
205
208
|
}
|
|
206
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Create a TURN Permission for a peer
|
|
212
|
+
* @param {string} peerAddress - Peer IP address
|
|
213
|
+
* @returns {Promise<void>}
|
|
214
|
+
*/
|
|
215
|
+
async createPermission(peerAddress) {
|
|
216
|
+
if (!this.username || !this.credential) {
|
|
217
|
+
throw new Error('TURN requires username and credential');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const transactionId = crypto.randomBytes(12);
|
|
221
|
+
const request = this._createCreatePermissionRequest(transactionId, peerAddress);
|
|
222
|
+
|
|
223
|
+
await this._sendRequest(request, transactionId, 'createPermission');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Send data to a peer via TURN Send Indication
|
|
228
|
+
* @param {string} peerAddress - Peer IP address
|
|
229
|
+
* @param {number} peerPort - Peer port
|
|
230
|
+
* @param {Buffer} data - Data to send
|
|
231
|
+
* @returns {Promise<void>}
|
|
232
|
+
*/
|
|
233
|
+
async sendIndication(peerAddress, peerPort, data) {
|
|
234
|
+
if (!this.username || !this.credential) {
|
|
235
|
+
throw new Error('TURN requires username and credential');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const transactionId = crypto.randomBytes(12);
|
|
239
|
+
const indication = this._createSendIndication(transactionId, peerAddress, peerPort, data);
|
|
240
|
+
|
|
241
|
+
// Indications are fire-and-forget, no response expected
|
|
242
|
+
return new Promise((resolve, reject) => {
|
|
243
|
+
this.socket.send(indication, this.port, this.server, (err) => {
|
|
244
|
+
if (err) reject(err);
|
|
245
|
+
else resolve();
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
207
250
|
/**
|
|
208
251
|
* Send a TURN request
|
|
209
252
|
* @param {Buffer} request - Request message
|
|
@@ -307,6 +350,87 @@ class STUNClient {
|
|
|
307
350
|
return this._createMessage(STUN_MESSAGE_TYPES.ALLOCATE_REQUEST, transactionId, attributes, withAuth);
|
|
308
351
|
}
|
|
309
352
|
|
|
353
|
+
/**
|
|
354
|
+
* Create a TURN CreatePermission Request
|
|
355
|
+
* @param {Buffer} transactionId - Transaction ID
|
|
356
|
+
* @param {string} peerAddress - Peer IP address
|
|
357
|
+
* @returns {Buffer} STUN message
|
|
358
|
+
* @private
|
|
359
|
+
*/
|
|
360
|
+
_createCreatePermissionRequest(transactionId, peerAddress) {
|
|
361
|
+
const attributes = [];
|
|
362
|
+
|
|
363
|
+
// XOR-PEER-ADDRESS
|
|
364
|
+
const peerAttr = this._createXorPeerAddressAttribute(peerAddress, 0, transactionId);
|
|
365
|
+
attributes.push(peerAttr);
|
|
366
|
+
|
|
367
|
+
// Auth attributes
|
|
368
|
+
if (this.realm && this.nonce) {
|
|
369
|
+
attributes.push(this._createStringAttribute(STUN_ATTRIBUTES.USERNAME, this.username));
|
|
370
|
+
attributes.push(this._createStringAttribute(STUN_ATTRIBUTES.REALM, this.realm));
|
|
371
|
+
attributes.push(this._createStringAttribute(STUN_ATTRIBUTES.NONCE, this.nonce));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return this._createMessage(STUN_MESSAGE_TYPES.CREATE_PERMISSION_REQUEST, transactionId, attributes, true);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Create a TURN Send Indication
|
|
379
|
+
* @param {Buffer} transactionId - Transaction ID
|
|
380
|
+
* @param {string} peerAddress - Peer IP address
|
|
381
|
+
* @param {number} peerPort - Peer port
|
|
382
|
+
* @param {Buffer} data - Data to send
|
|
383
|
+
* @returns {Buffer} STUN message
|
|
384
|
+
* @private
|
|
385
|
+
*/
|
|
386
|
+
_createSendIndication(transactionId, peerAddress, peerPort, data) {
|
|
387
|
+
const attributes = [];
|
|
388
|
+
|
|
389
|
+
// XOR-PEER-ADDRESS
|
|
390
|
+
const peerAttr = this._createXorPeerAddressAttribute(peerAddress, peerPort, transactionId);
|
|
391
|
+
attributes.push(peerAttr);
|
|
392
|
+
|
|
393
|
+
// DATA
|
|
394
|
+
const dataAttr = Buffer.alloc(4 + data.length + (4 - (data.length % 4)) % 4);
|
|
395
|
+
dataAttr.writeUInt16BE(STUN_ATTRIBUTES.DATA, 0);
|
|
396
|
+
dataAttr.writeUInt16BE(data.length, 2);
|
|
397
|
+
data.copy(dataAttr, 4);
|
|
398
|
+
attributes.push(dataAttr);
|
|
399
|
+
|
|
400
|
+
return this._createMessage(STUN_MESSAGE_TYPES.SEND_INDICATION, transactionId, attributes, false);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Create XOR-PEER-ADDRESS attribute
|
|
405
|
+
* @param {string} address - IP address
|
|
406
|
+
* @param {number} port - Port
|
|
407
|
+
* @param {Buffer} transactionId - Transaction ID
|
|
408
|
+
* @returns {Buffer} Attribute buffer
|
|
409
|
+
* @private
|
|
410
|
+
*/
|
|
411
|
+
_createXorPeerAddressAttribute(address, port, transactionId) {
|
|
412
|
+
const family = 0x01; // IPv4
|
|
413
|
+
const buffer = Buffer.alloc(4 + 8); // Type(2) + Length(2) + Reserved(1) + Family(1) + Port(2) + Address(4)
|
|
414
|
+
|
|
415
|
+
buffer.writeUInt16BE(STUN_ATTRIBUTES.XOR_PEER_ADDRESS, 0);
|
|
416
|
+
buffer.writeUInt16BE(8, 2);
|
|
417
|
+
buffer.writeUInt8(0, 4);
|
|
418
|
+
buffer.writeUInt8(family, 5);
|
|
419
|
+
|
|
420
|
+
// XOR Port
|
|
421
|
+
const xorPort = port ^ (MAGIC_COOKIE >> 16);
|
|
422
|
+
buffer.writeUInt16BE(xorPort, 6);
|
|
423
|
+
|
|
424
|
+
// XOR Address
|
|
425
|
+
const parts = address.split('.').map(Number);
|
|
426
|
+
const addrInt = (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3];
|
|
427
|
+
const xorAddr = addrInt ^ MAGIC_COOKIE;
|
|
428
|
+
|
|
429
|
+
buffer.writeUInt32BE(xorAddr >>> 0, 8); // Ensure unsigned
|
|
430
|
+
|
|
431
|
+
return buffer;
|
|
432
|
+
}
|
|
433
|
+
|
|
310
434
|
/**
|
|
311
435
|
* Create a TURN Refresh Request
|
|
312
436
|
* @param {Buffer} transactionId - Transaction ID
|
|
@@ -483,6 +607,16 @@ class STUNClient {
|
|
|
483
607
|
});
|
|
484
608
|
this.transactions.delete(transactionKey);
|
|
485
609
|
}
|
|
610
|
+
// Handle Data Indication
|
|
611
|
+
else if (messageType === STUN_MESSAGE_TYPES.DATA_INDICATION) {
|
|
612
|
+
if (attributes.xorPeerAddress && attributes.data) {
|
|
613
|
+
this.emit('data', attributes.data, {
|
|
614
|
+
address: attributes.xorPeerAddress.address,
|
|
615
|
+
port: attributes.xorPeerAddress.port,
|
|
616
|
+
family: attributes.xorPeerAddress.family || 'IPv4'
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
}
|
|
486
620
|
// Handle error responses
|
|
487
621
|
else if (messageType === STUN_MESSAGE_TYPES.BINDING_ERROR_RESPONSE ||
|
|
488
622
|
messageType === STUN_MESSAGE_TYPES.ALLOCATE_ERROR_RESPONSE) {
|