node-rtc-connection 1.0.17 → 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 +212 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +212 -24
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/ice/RTCIceTransport.js +28 -8
- package/src/peerconnection/RTCPeerConnection.js +39 -12
- package/src/sdp/sdp-utils.js +1 -1
- package/src/stun/stun-client.js +135 -1
package/dist/index.mjs
CHANGED
|
@@ -813,6 +813,8 @@ function requireStunClient () {
|
|
|
813
813
|
const dgram = require$$0;
|
|
814
814
|
const crypto = require$$3;
|
|
815
815
|
|
|
816
|
+
const EventEmitter = require$$0$1;
|
|
817
|
+
|
|
816
818
|
/**
|
|
817
819
|
* STUN message types
|
|
818
820
|
*/
|
|
@@ -827,7 +829,12 @@ function requireStunClient () {
|
|
|
827
829
|
ALLOCATE_ERROR_RESPONSE: 0x0113,
|
|
828
830
|
|
|
829
831
|
REFRESH_REQUEST: 0x0004,
|
|
830
|
-
REFRESH_RESPONSE: 0x0104
|
|
832
|
+
REFRESH_RESPONSE: 0x0104,
|
|
833
|
+
|
|
834
|
+
SEND_INDICATION: 0x0016,
|
|
835
|
+
DATA_INDICATION: 0x0017,
|
|
836
|
+
|
|
837
|
+
CREATE_PERMISSION_REQUEST: 0x0008};
|
|
831
838
|
|
|
832
839
|
/**
|
|
833
840
|
* STUN attribute types
|
|
@@ -842,6 +849,8 @@ function requireStunClient () {
|
|
|
842
849
|
XOR_MAPPED_ADDRESS: 0x0020,
|
|
843
850
|
|
|
844
851
|
LIFETIME: 0x000D,
|
|
852
|
+
XOR_PEER_ADDRESS: 0x0012,
|
|
853
|
+
DATA: 0x0013,
|
|
845
854
|
XOR_RELAYED_ADDRESS: 0x0016,
|
|
846
855
|
REQUESTED_TRANSPORT: 0x0019};
|
|
847
856
|
|
|
@@ -851,7 +860,7 @@ function requireStunClient () {
|
|
|
851
860
|
* @class STUNClient
|
|
852
861
|
* @description STUN/TURN client for NAT traversal
|
|
853
862
|
*/
|
|
854
|
-
class STUNClient {
|
|
863
|
+
class STUNClient extends EventEmitter {
|
|
855
864
|
/**
|
|
856
865
|
* Create a STUN client
|
|
857
866
|
* @param {Object} options - Client options
|
|
@@ -863,6 +872,7 @@ function requireStunClient () {
|
|
|
863
872
|
* @param {Object} [options.params={}] - Additional query parameters from URL
|
|
864
873
|
*/
|
|
865
874
|
constructor(options) {
|
|
875
|
+
super();
|
|
866
876
|
this.server = options.server;
|
|
867
877
|
this.port = options.port;
|
|
868
878
|
this.username = options.username;
|
|
@@ -986,6 +996,46 @@ function requireStunClient () {
|
|
|
986
996
|
return this._sendRequest(request, transactionId, 'refresh');
|
|
987
997
|
}
|
|
988
998
|
|
|
999
|
+
/**
|
|
1000
|
+
* Create a TURN Permission for a peer
|
|
1001
|
+
* @param {string} peerAddress - Peer IP address
|
|
1002
|
+
* @returns {Promise<void>}
|
|
1003
|
+
*/
|
|
1004
|
+
async createPermission(peerAddress) {
|
|
1005
|
+
if (!this.username || !this.credential) {
|
|
1006
|
+
throw new Error('TURN requires username and credential');
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
const transactionId = crypto.randomBytes(12);
|
|
1010
|
+
const request = this._createCreatePermissionRequest(transactionId, peerAddress);
|
|
1011
|
+
|
|
1012
|
+
await this._sendRequest(request, transactionId, 'createPermission');
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* Send data to a peer via TURN Send Indication
|
|
1017
|
+
* @param {string} peerAddress - Peer IP address
|
|
1018
|
+
* @param {number} peerPort - Peer port
|
|
1019
|
+
* @param {Buffer} data - Data to send
|
|
1020
|
+
* @returns {Promise<void>}
|
|
1021
|
+
*/
|
|
1022
|
+
async sendIndication(peerAddress, peerPort, data) {
|
|
1023
|
+
if (!this.username || !this.credential) {
|
|
1024
|
+
throw new Error('TURN requires username and credential');
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
const transactionId = crypto.randomBytes(12);
|
|
1028
|
+
const indication = this._createSendIndication(transactionId, peerAddress, peerPort, data);
|
|
1029
|
+
|
|
1030
|
+
// Indications are fire-and-forget, no response expected
|
|
1031
|
+
return new Promise((resolve, reject) => {
|
|
1032
|
+
this.socket.send(indication, this.port, this.server, (err) => {
|
|
1033
|
+
if (err) reject(err);
|
|
1034
|
+
else resolve();
|
|
1035
|
+
});
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
|
|
989
1039
|
/**
|
|
990
1040
|
* Send a TURN request
|
|
991
1041
|
* @param {Buffer} request - Request message
|
|
@@ -1089,6 +1139,87 @@ function requireStunClient () {
|
|
|
1089
1139
|
return this._createMessage(STUN_MESSAGE_TYPES.ALLOCATE_REQUEST, transactionId, attributes, withAuth);
|
|
1090
1140
|
}
|
|
1091
1141
|
|
|
1142
|
+
/**
|
|
1143
|
+
* Create a TURN CreatePermission Request
|
|
1144
|
+
* @param {Buffer} transactionId - Transaction ID
|
|
1145
|
+
* @param {string} peerAddress - Peer IP address
|
|
1146
|
+
* @returns {Buffer} STUN message
|
|
1147
|
+
* @private
|
|
1148
|
+
*/
|
|
1149
|
+
_createCreatePermissionRequest(transactionId, peerAddress) {
|
|
1150
|
+
const attributes = [];
|
|
1151
|
+
|
|
1152
|
+
// XOR-PEER-ADDRESS
|
|
1153
|
+
const peerAttr = this._createXorPeerAddressAttribute(peerAddress, 0, transactionId);
|
|
1154
|
+
attributes.push(peerAttr);
|
|
1155
|
+
|
|
1156
|
+
// Auth attributes
|
|
1157
|
+
if (this.realm && this.nonce) {
|
|
1158
|
+
attributes.push(this._createStringAttribute(STUN_ATTRIBUTES.USERNAME, this.username));
|
|
1159
|
+
attributes.push(this._createStringAttribute(STUN_ATTRIBUTES.REALM, this.realm));
|
|
1160
|
+
attributes.push(this._createStringAttribute(STUN_ATTRIBUTES.NONCE, this.nonce));
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
return this._createMessage(STUN_MESSAGE_TYPES.CREATE_PERMISSION_REQUEST, transactionId, attributes, true);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
/**
|
|
1167
|
+
* Create a TURN Send Indication
|
|
1168
|
+
* @param {Buffer} transactionId - Transaction ID
|
|
1169
|
+
* @param {string} peerAddress - Peer IP address
|
|
1170
|
+
* @param {number} peerPort - Peer port
|
|
1171
|
+
* @param {Buffer} data - Data to send
|
|
1172
|
+
* @returns {Buffer} STUN message
|
|
1173
|
+
* @private
|
|
1174
|
+
*/
|
|
1175
|
+
_createSendIndication(transactionId, peerAddress, peerPort, data) {
|
|
1176
|
+
const attributes = [];
|
|
1177
|
+
|
|
1178
|
+
// XOR-PEER-ADDRESS
|
|
1179
|
+
const peerAttr = this._createXorPeerAddressAttribute(peerAddress, peerPort, transactionId);
|
|
1180
|
+
attributes.push(peerAttr);
|
|
1181
|
+
|
|
1182
|
+
// DATA
|
|
1183
|
+
const dataAttr = Buffer.alloc(4 + data.length + (4 - (data.length % 4)) % 4);
|
|
1184
|
+
dataAttr.writeUInt16BE(STUN_ATTRIBUTES.DATA, 0);
|
|
1185
|
+
dataAttr.writeUInt16BE(data.length, 2);
|
|
1186
|
+
data.copy(dataAttr, 4);
|
|
1187
|
+
attributes.push(dataAttr);
|
|
1188
|
+
|
|
1189
|
+
return this._createMessage(STUN_MESSAGE_TYPES.SEND_INDICATION, transactionId, attributes, false);
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
/**
|
|
1193
|
+
* Create XOR-PEER-ADDRESS attribute
|
|
1194
|
+
* @param {string} address - IP address
|
|
1195
|
+
* @param {number} port - Port
|
|
1196
|
+
* @param {Buffer} transactionId - Transaction ID
|
|
1197
|
+
* @returns {Buffer} Attribute buffer
|
|
1198
|
+
* @private
|
|
1199
|
+
*/
|
|
1200
|
+
_createXorPeerAddressAttribute(address, port, transactionId) {
|
|
1201
|
+
const family = 0x01; // IPv4
|
|
1202
|
+
const buffer = Buffer.alloc(4 + 8); // Type(2) + Length(2) + Reserved(1) + Family(1) + Port(2) + Address(4)
|
|
1203
|
+
|
|
1204
|
+
buffer.writeUInt16BE(STUN_ATTRIBUTES.XOR_PEER_ADDRESS, 0);
|
|
1205
|
+
buffer.writeUInt16BE(8, 2);
|
|
1206
|
+
buffer.writeUInt8(0, 4);
|
|
1207
|
+
buffer.writeUInt8(family, 5);
|
|
1208
|
+
|
|
1209
|
+
// XOR Port
|
|
1210
|
+
const xorPort = port ^ (MAGIC_COOKIE >> 16);
|
|
1211
|
+
buffer.writeUInt16BE(xorPort, 6);
|
|
1212
|
+
|
|
1213
|
+
// XOR Address
|
|
1214
|
+
const parts = address.split('.').map(Number);
|
|
1215
|
+
const addrInt = (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3];
|
|
1216
|
+
const xorAddr = addrInt ^ MAGIC_COOKIE;
|
|
1217
|
+
|
|
1218
|
+
buffer.writeUInt32BE(xorAddr >>> 0, 8); // Ensure unsigned
|
|
1219
|
+
|
|
1220
|
+
return buffer;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1092
1223
|
/**
|
|
1093
1224
|
* Create a TURN Refresh Request
|
|
1094
1225
|
* @param {Buffer} transactionId - Transaction ID
|
|
@@ -1265,6 +1396,16 @@ function requireStunClient () {
|
|
|
1265
1396
|
});
|
|
1266
1397
|
this.transactions.delete(transactionKey);
|
|
1267
1398
|
}
|
|
1399
|
+
// Handle Data Indication
|
|
1400
|
+
else if (messageType === STUN_MESSAGE_TYPES.DATA_INDICATION) {
|
|
1401
|
+
if (attributes.xorPeerAddress && attributes.data) {
|
|
1402
|
+
this.emit('data', attributes.data, {
|
|
1403
|
+
address: attributes.xorPeerAddress.address,
|
|
1404
|
+
port: attributes.xorPeerAddress.port,
|
|
1405
|
+
family: attributes.xorPeerAddress.family || 'IPv4'
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1268
1409
|
// Handle error responses
|
|
1269
1410
|
else if (messageType === STUN_MESSAGE_TYPES.BINDING_ERROR_RESPONSE ||
|
|
1270
1411
|
messageType === STUN_MESSAGE_TYPES.ALLOCATE_ERROR_RESPONSE) {
|
|
@@ -1791,7 +1932,10 @@ function requireRTCIceTransport () {
|
|
|
1791
1932
|
|
|
1792
1933
|
// Validate that candidate has required properties for connectivity checks
|
|
1793
1934
|
if (!parsedCandidate.address || parsedCandidate.port === null || parsedCandidate.port === undefined) {
|
|
1794
|
-
|
|
1935
|
+
// Only warn if it's not an empty candidate (which signals end-of-candidates)
|
|
1936
|
+
if (parsedCandidate.candidate !== '') {
|
|
1937
|
+
console.warn('Remote candidate missing address or port, skipping connectivity checks');
|
|
1938
|
+
}
|
|
1795
1939
|
// Store the original candidate for compatibility
|
|
1796
1940
|
this._remoteCandidates.push(candidate);
|
|
1797
1941
|
return;
|
|
@@ -2118,6 +2262,11 @@ function requireRTCIceTransport () {
|
|
|
2118
2262
|
// Store TURN client as the "socket" for this relay candidate
|
|
2119
2263
|
this._sockets.set(foundation, { type: 'turn', client: turnClient });
|
|
2120
2264
|
|
|
2265
|
+
// Handle incoming data from TURN
|
|
2266
|
+
turnClient.on('data', (data, peer) => {
|
|
2267
|
+
this._handleSocketMessage(data, peer, candidate);
|
|
2268
|
+
});
|
|
2269
|
+
|
|
2121
2270
|
this._addLocalCandidate(candidate);
|
|
2122
2271
|
|
|
2123
2272
|
// Keep allocation alive
|
|
@@ -2194,8 +2343,8 @@ function requireRTCIceTransport () {
|
|
|
2194
2343
|
*/
|
|
2195
2344
|
_sendConnectivityCheck(pair) {
|
|
2196
2345
|
const socket = this._sockets.get(pair.local.foundation);
|
|
2197
|
-
if (!socket
|
|
2198
|
-
return;
|
|
2346
|
+
if (!socket) {
|
|
2347
|
+
return;
|
|
2199
2348
|
}
|
|
2200
2349
|
|
|
2201
2350
|
// Validate remote candidate has required properties
|
|
@@ -2212,11 +2361,23 @@ function requireRTCIceTransport () {
|
|
|
2212
2361
|
const request = this._createBindingRequest(transactionId);
|
|
2213
2362
|
|
|
2214
2363
|
try {
|
|
2215
|
-
socket.
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2364
|
+
if (socket.type === 'turn') {
|
|
2365
|
+
const turnClient = socket.client;
|
|
2366
|
+
// Create permission and send indication
|
|
2367
|
+
turnClient.createPermission(pair.remote.address)
|
|
2368
|
+
.then(() => {
|
|
2369
|
+
return turnClient.sendIndication(pair.remote.address, pair.remote.port, request);
|
|
2370
|
+
})
|
|
2371
|
+
.catch(err => {
|
|
2372
|
+
// Suppress errors for now as this happens frequently during connection
|
|
2373
|
+
});
|
|
2374
|
+
} else {
|
|
2375
|
+
socket.send(request, pair.remote.port, pair.remote.address, (err) => {
|
|
2376
|
+
if (err) {
|
|
2377
|
+
console.error(`Connectivity check failed for ${pair.remote.address}:${pair.remote.port}:`, err);
|
|
2378
|
+
}
|
|
2379
|
+
});
|
|
2380
|
+
}
|
|
2220
2381
|
} catch (err) {
|
|
2221
2382
|
console.error(`Error sending connectivity check to ${pair.remote.address || 'unknown'}:${pair.remote.port || 'unknown'}:`, err);
|
|
2222
2383
|
}
|
|
@@ -3855,7 +4016,7 @@ function requireSdpUtils () {
|
|
|
3855
4016
|
for (const line of lines) {
|
|
3856
4017
|
if (line.startsWith('a=candidate:')) {
|
|
3857
4018
|
candidates.push({
|
|
3858
|
-
candidate: line,
|
|
4019
|
+
candidate: line.substring(2),
|
|
3859
4020
|
sdpMid: '0',
|
|
3860
4021
|
sdpMLineIndex: 0
|
|
3861
4022
|
});
|
|
@@ -4693,6 +4854,16 @@ function requireRTCPeerConnection () {
|
|
|
4693
4854
|
* @private
|
|
4694
4855
|
*/
|
|
4695
4856
|
_openDataChannels() {
|
|
4857
|
+
// Check if network transport has active connections
|
|
4858
|
+
const hasConnections = this._networkTransport &&
|
|
4859
|
+
this._networkTransport.tcpTransport &&
|
|
4860
|
+
this._networkTransport.tcpTransport.connections.size > 0;
|
|
4861
|
+
|
|
4862
|
+
if (!hasConnections) {
|
|
4863
|
+
// Network not ready yet, channels will be opened when connection establishes
|
|
4864
|
+
return;
|
|
4865
|
+
}
|
|
4866
|
+
|
|
4696
4867
|
for (const channel of this._dataChannels.values()) {
|
|
4697
4868
|
if (channel.readyState === 'connecting') {
|
|
4698
4869
|
this._connectChannelToNetwork(channel);
|
|
@@ -5060,6 +5231,11 @@ function requireRTCPeerConnection () {
|
|
|
5060
5231
|
console.error('Failed to establish network connection:', error);
|
|
5061
5232
|
}
|
|
5062
5233
|
|
|
5234
|
+
// Snapshot the candidates BEFORE starting ICE to avoid race condition
|
|
5235
|
+
// If addIceCandidate runs while start() is yielding, it will see transport running and add the candidate.
|
|
5236
|
+
// If we snapshot after start(), we might add the same candidate twice.
|
|
5237
|
+
const candidatesToAdd = [...this._remoteIceCandidates];
|
|
5238
|
+
|
|
5063
5239
|
// Start ICE
|
|
5064
5240
|
if (iceParams.usernameFragment && iceParams.password) {
|
|
5065
5241
|
try {
|
|
@@ -5070,7 +5246,7 @@ function requireRTCPeerConnection () {
|
|
|
5070
5246
|
}
|
|
5071
5247
|
|
|
5072
5248
|
// Add remote candidates
|
|
5073
|
-
for (const candidate of
|
|
5249
|
+
for (const candidate of candidatesToAdd) {
|
|
5074
5250
|
try {
|
|
5075
5251
|
// Parse candidate string (simplified)
|
|
5076
5252
|
await this._iceTransport.addRemoteCandidate(candidate);
|
|
@@ -5082,15 +5258,8 @@ function requireRTCPeerConnection () {
|
|
|
5082
5258
|
// Open data channels when connection is established
|
|
5083
5259
|
this._sctpTransport.once('statechange', () => {
|
|
5084
5260
|
if (this._sctpTransport.state === 'connected') {
|
|
5085
|
-
for
|
|
5086
|
-
|
|
5087
|
-
// Hook up channel to network transport first
|
|
5088
|
-
this._connectChannelToNetwork(channel);
|
|
5089
|
-
|
|
5090
|
-
// Then set state to open (emits 'open' event)
|
|
5091
|
-
channel._setStateToOpen();
|
|
5092
|
-
}
|
|
5093
|
-
}
|
|
5261
|
+
// Wait for network to be ready before opening channels
|
|
5262
|
+
this._openDataChannels();
|
|
5094
5263
|
}
|
|
5095
5264
|
});
|
|
5096
5265
|
}
|
|
@@ -5137,7 +5306,7 @@ function requireRTCPeerConnection () {
|
|
|
5137
5306
|
throw new Error('RTCPeerConnection is closed');
|
|
5138
5307
|
}
|
|
5139
5308
|
|
|
5140
|
-
if (!candidate) {
|
|
5309
|
+
if (!candidate || (candidate.candidate === '')) {
|
|
5141
5310
|
// End of candidates signal
|
|
5142
5311
|
this._iceGatheringState = RTCIceGatheringState.COMPLETE;
|
|
5143
5312
|
this.emit('icegatheringstatechange');
|
|
@@ -5150,7 +5319,26 @@ function requireRTCPeerConnection () {
|
|
|
5150
5319
|
this._remoteIceCandidates.push(candidate);
|
|
5151
5320
|
|
|
5152
5321
|
// If connection is already started, add candidate immediately
|
|
5153
|
-
if (
|
|
5322
|
+
// Check if transport is actually started (not just initialized)
|
|
5323
|
+
// We can infer this if we have remote description AND the transport state is not 'new'
|
|
5324
|
+
// Or simply try/catch and ignore "not started" error, but better to check.
|
|
5325
|
+
// Since we don't have public access to _started, we rely on state.
|
|
5326
|
+
// However, state might be 'new' but start() was called? No, start() sets state to checking.
|
|
5327
|
+
|
|
5328
|
+
// Actually, _startConnection calls start().
|
|
5329
|
+
// If we are Answerer, we set remote offer. _startConnection is NOT called yet.
|
|
5330
|
+
// It is called when we set local answer.
|
|
5331
|
+
// So we should NOT add candidates yet.
|
|
5332
|
+
|
|
5333
|
+
// If we are Offerer, we set remote answer. _startConnection IS called.
|
|
5334
|
+
|
|
5335
|
+
// So we should only add if we have both descriptions (Stable state)?
|
|
5336
|
+
// Or if the transport is started.
|
|
5337
|
+
|
|
5338
|
+
// Let's check if we are in a state where transport should be running.
|
|
5339
|
+
const isTransportRunning = this._iceTransport.state !== 'new' && this._iceTransport.state !== 'closed';
|
|
5340
|
+
|
|
5341
|
+
if (isTransportRunning) {
|
|
5154
5342
|
try {
|
|
5155
5343
|
await this._iceTransport.addRemoteCandidate(candidate);
|
|
5156
5344
|
} catch (error) {
|
|
@@ -5317,7 +5505,7 @@ function requireRTCPeerConnection () {
|
|
|
5317
5505
|
return RTCPeerConnection_1;
|
|
5318
5506
|
}
|
|
5319
5507
|
|
|
5320
|
-
var version = "1.0.
|
|
5508
|
+
var version = "1.0.19";
|
|
5321
5509
|
var require$$10 = {
|
|
5322
5510
|
version: version};
|
|
5323
5511
|
|