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.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
  });
@@ -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 this._remoteIceCandidates) {
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 (const channel of this._dataChannels.values()) {
5086
- if (channel.readyState === 'connecting') {
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 (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) {
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.17";
5508
+ var version = "1.0.19";
5321
5509
  var require$$10 = {
5322
5510
  version: version};
5323
5511