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