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.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
|
-
|
|
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
|
|
2200
|
-
return;
|
|
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.
|
|
2218
|
-
|
|
2219
|
-
|
|
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
|
|
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
|
|
5088
|
-
|
|
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 (
|
|
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.
|
|
5510
|
+
var version = "1.0.19";
|
|
5323
5511
|
var require$$10 = {
|
|
5324
5512
|
version: version};
|
|
5325
5513
|
|