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.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
- console.warn('Remote candidate missing address or port, skipping connectivity checks');
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 || socket.type === 'turn') {
2198
- return; // Skip TURN candidates for now
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.send(request, pair.remote.port, pair.remote.address, (err) => {
2216
- if (err) {
2217
- console.error(`Connectivity check failed for ${pair.remote.address}:${pair.remote.port}:`, err);
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
  });
@@ -5070,6 +5231,11 @@ function requireRTCPeerConnection () {
5070
5231
  console.error('Failed to establish network connection:', error);
5071
5232
  }
5072
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
+
5073
5239
  // Start ICE
5074
5240
  if (iceParams.usernameFragment && iceParams.password) {
5075
5241
  try {
@@ -5080,7 +5246,7 @@ function requireRTCPeerConnection () {
5080
5246
  }
5081
5247
 
5082
5248
  // Add remote candidates
5083
- for (const candidate of this._remoteIceCandidates) {
5249
+ for (const candidate of candidatesToAdd) {
5084
5250
  try {
5085
5251
  // Parse candidate string (simplified)
5086
5252
  await this._iceTransport.addRemoteCandidate(candidate);
@@ -5140,7 +5306,7 @@ function requireRTCPeerConnection () {
5140
5306
  throw new Error('RTCPeerConnection is closed');
5141
5307
  }
5142
5308
 
5143
- if (!candidate) {
5309
+ if (!candidate || (candidate.candidate === '')) {
5144
5310
  // End of candidates signal
5145
5311
  this._iceGatheringState = RTCIceGatheringState.COMPLETE;
5146
5312
  this.emit('icegatheringstatechange');
@@ -5153,7 +5319,26 @@ function requireRTCPeerConnection () {
5153
5319
  this._remoteIceCandidates.push(candidate);
5154
5320
 
5155
5321
  // If connection is already started, add candidate immediately
5156
- if (this._remoteDescription) {
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) {
5157
5342
  try {
5158
5343
  await this._iceTransport.addRemoteCandidate(candidate);
5159
5344
  } catch (error) {
@@ -5320,7 +5505,7 @@ function requireRTCPeerConnection () {
5320
5505
  return RTCPeerConnection_1;
5321
5506
  }
5322
5507
 
5323
- var version = "1.0.18";
5508
+ var version = "1.0.19";
5324
5509
  var require$$10 = {
5325
5510
  version: version};
5326
5511