node-rtc-connection 1.0.12 → 1.0.14
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/README.md +355 -289
- package/dist/index.cjs +4318 -3095
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +4318 -3095
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
- package/src/datachannel/RTCDataChannel.js +354 -0
- package/src/dtls/RTCCertificate.js +310 -0
- package/src/dtls/RTCDtlsTransport.js +247 -0
- package/src/foundation/ByteBufferQueue.js +235 -0
- package/src/foundation/RTCError.js +226 -0
- package/src/ice/RTCIceCandidate.js +301 -0
- package/src/ice/RTCIceTransport.js +956 -0
- package/src/index.d.ts +316 -145
- package/src/index.js +78 -45
- package/src/network/network-transport.js +478 -0
- package/src/peerconnection/RTCPeerConnection.js +847 -0
- package/src/sctp/RTCSctpTransport.js +253 -0
- package/src/sdp/RTCSessionDescription.js +102 -0
- package/src/sdp/sdp-utils.js +224 -0
- package/src/stun/stun-client.js +643 -0
- package/src/ICEGatherer.js +0 -341
- package/src/NativePeerConnectionFactory.js +0 -1044
- package/src/RTCDataChannel.js +0 -346
- package/src/RTCDataChannelEvent.js +0 -50
- package/src/RTCError.js +0 -66
- package/src/RTCIceCandidate.js +0 -184
- package/src/RTCPeerConnection.js +0 -505
- package/src/RTCPeerConnectionIceEvent.js +0 -58
- package/src/RTCSessionDescription.js +0 -62
- package/src/STUNClient.js +0 -222
- package/src/SecureConnection.js +0 -298
- package/src/TURNClient.js +0 -561
- package/src/UDPTransport.js +0 -236
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file stun-client.js
|
|
3
|
+
* @description STUN (Session Traversal Utilities for NAT) client implementation
|
|
4
|
+
* @module stun/stun-client
|
|
5
|
+
*
|
|
6
|
+
* STUN Protocol: RFC 5389
|
|
7
|
+
* TURN Protocol: RFC 5766
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const dgram = require('dgram');
|
|
13
|
+
const crypto = require('crypto');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* STUN message types
|
|
17
|
+
*/
|
|
18
|
+
const STUN_MESSAGE_TYPES = {
|
|
19
|
+
BINDING_REQUEST: 0x0001,
|
|
20
|
+
BINDING_RESPONSE: 0x0101,
|
|
21
|
+
BINDING_ERROR_RESPONSE: 0x0111,
|
|
22
|
+
|
|
23
|
+
// TURN
|
|
24
|
+
ALLOCATE_REQUEST: 0x0003,
|
|
25
|
+
ALLOCATE_RESPONSE: 0x0103,
|
|
26
|
+
ALLOCATE_ERROR_RESPONSE: 0x0113,
|
|
27
|
+
|
|
28
|
+
REFRESH_REQUEST: 0x0004,
|
|
29
|
+
REFRESH_RESPONSE: 0x0104,
|
|
30
|
+
|
|
31
|
+
SEND_INDICATION: 0x0016,
|
|
32
|
+
DATA_INDICATION: 0x0017,
|
|
33
|
+
|
|
34
|
+
CREATE_PERMISSION_REQUEST: 0x0008,
|
|
35
|
+
CREATE_PERMISSION_RESPONSE: 0x0108,
|
|
36
|
+
|
|
37
|
+
CHANNEL_BIND_REQUEST: 0x0009,
|
|
38
|
+
CHANNEL_BIND_RESPONSE: 0x0109
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* STUN attribute types
|
|
43
|
+
*/
|
|
44
|
+
const STUN_ATTRIBUTES = {
|
|
45
|
+
MAPPED_ADDRESS: 0x0001,
|
|
46
|
+
USERNAME: 0x0006,
|
|
47
|
+
MESSAGE_INTEGRITY: 0x0008,
|
|
48
|
+
ERROR_CODE: 0x0009,
|
|
49
|
+
UNKNOWN_ATTRIBUTES: 0x000A,
|
|
50
|
+
REALM: 0x0014,
|
|
51
|
+
NONCE: 0x0015,
|
|
52
|
+
XOR_MAPPED_ADDRESS: 0x0020,
|
|
53
|
+
|
|
54
|
+
// TURN
|
|
55
|
+
CHANNEL_NUMBER: 0x000C,
|
|
56
|
+
LIFETIME: 0x000D,
|
|
57
|
+
XOR_PEER_ADDRESS: 0x0012,
|
|
58
|
+
DATA: 0x0013,
|
|
59
|
+
XOR_RELAYED_ADDRESS: 0x0016,
|
|
60
|
+
REQUESTED_TRANSPORT: 0x0019,
|
|
61
|
+
|
|
62
|
+
SOFTWARE: 0x8022,
|
|
63
|
+
FINGERPRINT: 0x8028
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const MAGIC_COOKIE = 0x2112A442;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @class STUNClient
|
|
70
|
+
* @description STUN/TURN client for NAT traversal
|
|
71
|
+
*/
|
|
72
|
+
class STUNClient {
|
|
73
|
+
/**
|
|
74
|
+
* Create a STUN client
|
|
75
|
+
* @param {Object} options - Client options
|
|
76
|
+
* @param {string} options.server - STUN/TURN server address
|
|
77
|
+
* @param {number} options.port - Server port
|
|
78
|
+
* @param {string} [options.username] - TURN username
|
|
79
|
+
* @param {string} [options.credential] - TURN password
|
|
80
|
+
* @param {string} [options.transport='udp'] - Transport protocol (udp/tcp)
|
|
81
|
+
* @param {Object} [options.params={}] - Additional query parameters from URL
|
|
82
|
+
*/
|
|
83
|
+
constructor(options) {
|
|
84
|
+
this.server = options.server;
|
|
85
|
+
this.port = options.port;
|
|
86
|
+
this.username = options.username;
|
|
87
|
+
this.credential = options.credential;
|
|
88
|
+
this.transport = options.transport || 'udp';
|
|
89
|
+
this.params = options.params || {};
|
|
90
|
+
|
|
91
|
+
this.socket = null;
|
|
92
|
+
this.transactions = new Map();
|
|
93
|
+
this.realm = null;
|
|
94
|
+
this.nonce = null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Connect to the STUN/TURN server
|
|
99
|
+
* @returns {Promise<void>}
|
|
100
|
+
*/
|
|
101
|
+
async connect() {
|
|
102
|
+
if (this.socket) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return new Promise((resolve, reject) => {
|
|
107
|
+
this.socket = dgram.createSocket('udp4');
|
|
108
|
+
|
|
109
|
+
this.socket.on('message', (msg, rinfo) => {
|
|
110
|
+
this._handleMessage(msg, rinfo);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
this.socket.on('error', (err) => {
|
|
114
|
+
console.error('STUN socket error:', err);
|
|
115
|
+
reject(err);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
this.socket.bind(() => {
|
|
119
|
+
resolve();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Send a STUN Binding Request to get reflexive address
|
|
126
|
+
* @returns {Promise<Object>} Reflexive address info
|
|
127
|
+
*/
|
|
128
|
+
async getReflexiveAddress() {
|
|
129
|
+
await this.connect();
|
|
130
|
+
|
|
131
|
+
const transactionId = crypto.randomBytes(12);
|
|
132
|
+
const request = this._createBindingRequest(transactionId);
|
|
133
|
+
|
|
134
|
+
return new Promise((resolve, reject) => {
|
|
135
|
+
const timeout = setTimeout(() => {
|
|
136
|
+
this.transactions.delete(transactionId.toString('hex'));
|
|
137
|
+
reject(new Error('STUN request timeout'));
|
|
138
|
+
}, 5000);
|
|
139
|
+
|
|
140
|
+
this.transactions.set(transactionId.toString('hex'), {
|
|
141
|
+
resolve: (result) => {
|
|
142
|
+
clearTimeout(timeout);
|
|
143
|
+
resolve(result);
|
|
144
|
+
},
|
|
145
|
+
reject: (error) => {
|
|
146
|
+
clearTimeout(timeout);
|
|
147
|
+
reject(error);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
this.socket.send(request, this.port, this.server, (err) => {
|
|
152
|
+
if (err) {
|
|
153
|
+
clearTimeout(timeout);
|
|
154
|
+
this.transactions.delete(transactionId.toString('hex'));
|
|
155
|
+
reject(err);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Send a TURN Allocate Request to get relay address
|
|
163
|
+
* @param {number} [lifetime=600] - Allocation lifetime in seconds
|
|
164
|
+
* @returns {Promise<Object>} Relay address info
|
|
165
|
+
*/
|
|
166
|
+
async allocateRelay(lifetime = 600) {
|
|
167
|
+
if (!this.username || !this.credential) {
|
|
168
|
+
throw new Error('TURN requires username and credential');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
await this.connect();
|
|
172
|
+
|
|
173
|
+
let transactionId = crypto.randomBytes(12);
|
|
174
|
+
let request = this._createAllocateRequest(transactionId, lifetime);
|
|
175
|
+
|
|
176
|
+
// First attempt without credentials to get realm and nonce
|
|
177
|
+
try {
|
|
178
|
+
return await this._sendRequest(request, transactionId, 'allocate');
|
|
179
|
+
} catch (error) {
|
|
180
|
+
// If we get 401 Unauthorized, retry with credentials
|
|
181
|
+
if (error.message.includes('401') && this.realm && this.nonce) {
|
|
182
|
+
// Create new transaction ID for retry
|
|
183
|
+
transactionId = crypto.randomBytes(12);
|
|
184
|
+
request = this._createAllocateRequest(transactionId, lifetime, true);
|
|
185
|
+
return await this._sendRequest(request, transactionId, 'allocate');
|
|
186
|
+
}
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Send a TURN Refresh Request to keep allocation alive
|
|
193
|
+
* @param {number} [lifetime=600] - Allocation lifetime in seconds
|
|
194
|
+
* @returns {Promise<Object>} Updated allocation info
|
|
195
|
+
*/
|
|
196
|
+
async refreshAllocation(lifetime = 600) {
|
|
197
|
+
if (!this.username || !this.credential) {
|
|
198
|
+
throw new Error('TURN requires username and credential');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const transactionId = crypto.randomBytes(12);
|
|
202
|
+
const request = this._createRefreshRequest(transactionId, lifetime);
|
|
203
|
+
|
|
204
|
+
return this._sendRequest(request, transactionId, 'refresh');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Send a TURN request
|
|
209
|
+
* @param {Buffer} request - Request message
|
|
210
|
+
* @param {Buffer} transactionId - Transaction ID
|
|
211
|
+
* @param {string} requestType - Type of request
|
|
212
|
+
* @returns {Promise<Object>}
|
|
213
|
+
* @private
|
|
214
|
+
*/
|
|
215
|
+
_sendRequest(request, transactionId, requestType) {
|
|
216
|
+
return new Promise((resolve, reject) => {
|
|
217
|
+
const timeout = setTimeout(() => {
|
|
218
|
+
this.transactions.delete(transactionId.toString('hex'));
|
|
219
|
+
reject(new Error(`${requestType} request timeout`));
|
|
220
|
+
}, 5000);
|
|
221
|
+
|
|
222
|
+
this.transactions.set(transactionId.toString('hex'), {
|
|
223
|
+
type: requestType,
|
|
224
|
+
resolve: (result) => {
|
|
225
|
+
clearTimeout(timeout);
|
|
226
|
+
resolve(result);
|
|
227
|
+
},
|
|
228
|
+
reject: (error) => {
|
|
229
|
+
clearTimeout(timeout);
|
|
230
|
+
reject(error);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
this.socket.send(request, this.port, this.server, (err) => {
|
|
235
|
+
if (err) {
|
|
236
|
+
clearTimeout(timeout);
|
|
237
|
+
this.transactions.delete(transactionId.toString('hex'));
|
|
238
|
+
reject(err);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Create a STUN Binding Request
|
|
246
|
+
* @param {Buffer} transactionId - Transaction ID
|
|
247
|
+
* @returns {Buffer} STUN message
|
|
248
|
+
* @private
|
|
249
|
+
*/
|
|
250
|
+
_createBindingRequest(transactionId) {
|
|
251
|
+
const header = Buffer.alloc(20);
|
|
252
|
+
|
|
253
|
+
// Message Type (2 bytes)
|
|
254
|
+
header.writeUInt16BE(STUN_MESSAGE_TYPES.BINDING_REQUEST, 0);
|
|
255
|
+
|
|
256
|
+
// Message Length (2 bytes) - 0 for now, no attributes
|
|
257
|
+
header.writeUInt16BE(0, 2);
|
|
258
|
+
|
|
259
|
+
// Magic Cookie (4 bytes)
|
|
260
|
+
header.writeUInt32BE(MAGIC_COOKIE, 4);
|
|
261
|
+
|
|
262
|
+
// Transaction ID (12 bytes)
|
|
263
|
+
transactionId.copy(header, 8);
|
|
264
|
+
|
|
265
|
+
return header;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Create a TURN Allocate Request
|
|
270
|
+
* @param {Buffer} transactionId - Transaction ID
|
|
271
|
+
* @param {number} lifetime - Allocation lifetime in seconds
|
|
272
|
+
* @param {boolean} withAuth - Include authentication
|
|
273
|
+
* @returns {Buffer} STUN message
|
|
274
|
+
* @private
|
|
275
|
+
*/
|
|
276
|
+
_createAllocateRequest(transactionId, lifetime, withAuth = false) {
|
|
277
|
+
const attributes = [];
|
|
278
|
+
|
|
279
|
+
// REQUESTED-TRANSPORT (UDP = 17)
|
|
280
|
+
const transport = Buffer.alloc(8);
|
|
281
|
+
transport.writeUInt16BE(STUN_ATTRIBUTES.REQUESTED_TRANSPORT, 0);
|
|
282
|
+
transport.writeUInt16BE(4, 2);
|
|
283
|
+
transport.writeUInt8(17, 4); // UDP
|
|
284
|
+
attributes.push(transport);
|
|
285
|
+
|
|
286
|
+
// LIFETIME
|
|
287
|
+
const lifetimeAttr = Buffer.alloc(8);
|
|
288
|
+
lifetimeAttr.writeUInt16BE(STUN_ATTRIBUTES.LIFETIME, 0);
|
|
289
|
+
lifetimeAttr.writeUInt16BE(4, 2);
|
|
290
|
+
lifetimeAttr.writeUInt32BE(lifetime, 4);
|
|
291
|
+
attributes.push(lifetimeAttr);
|
|
292
|
+
|
|
293
|
+
if (withAuth && this.realm && this.nonce) {
|
|
294
|
+
// USERNAME
|
|
295
|
+
const usernameAttr = this._createStringAttribute(STUN_ATTRIBUTES.USERNAME, this.username);
|
|
296
|
+
attributes.push(usernameAttr);
|
|
297
|
+
|
|
298
|
+
// REALM
|
|
299
|
+
const realmAttr = this._createStringAttribute(STUN_ATTRIBUTES.REALM, this.realm);
|
|
300
|
+
attributes.push(realmAttr);
|
|
301
|
+
|
|
302
|
+
// NONCE
|
|
303
|
+
const nonceAttr = this._createStringAttribute(STUN_ATTRIBUTES.NONCE, this.nonce);
|
|
304
|
+
attributes.push(nonceAttr);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return this._createMessage(STUN_MESSAGE_TYPES.ALLOCATE_REQUEST, transactionId, attributes, withAuth);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Create a TURN Refresh Request
|
|
312
|
+
* @param {Buffer} transactionId - Transaction ID
|
|
313
|
+
* @param {number} lifetime - Allocation lifetime in seconds
|
|
314
|
+
* @returns {Buffer} STUN message
|
|
315
|
+
* @private
|
|
316
|
+
*/
|
|
317
|
+
_createRefreshRequest(transactionId, lifetime) {
|
|
318
|
+
const attributes = [];
|
|
319
|
+
|
|
320
|
+
// LIFETIME
|
|
321
|
+
const lifetimeAttr = Buffer.alloc(8);
|
|
322
|
+
lifetimeAttr.writeUInt16BE(STUN_ATTRIBUTES.LIFETIME, 0);
|
|
323
|
+
lifetimeAttr.writeUInt16BE(4, 2);
|
|
324
|
+
lifetimeAttr.writeUInt32BE(lifetime, 4);
|
|
325
|
+
attributes.push(lifetimeAttr);
|
|
326
|
+
|
|
327
|
+
// USERNAME
|
|
328
|
+
const usernameAttr = this._createStringAttribute(STUN_ATTRIBUTES.USERNAME, this.username);
|
|
329
|
+
attributes.push(usernameAttr);
|
|
330
|
+
|
|
331
|
+
// REALM
|
|
332
|
+
if (this.realm) {
|
|
333
|
+
const realmAttr = this._createStringAttribute(STUN_ATTRIBUTES.REALM, this.realm);
|
|
334
|
+
attributes.push(realmAttr);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// NONCE
|
|
338
|
+
if (this.nonce) {
|
|
339
|
+
const nonceAttr = this._createStringAttribute(STUN_ATTRIBUTES.NONCE, this.nonce);
|
|
340
|
+
attributes.push(nonceAttr);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return this._createMessage(STUN_MESSAGE_TYPES.REFRESH_REQUEST, transactionId, attributes, true);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Create a STUN message with attributes
|
|
348
|
+
* @param {number} messageType - Message type
|
|
349
|
+
* @param {Buffer} transactionId - Transaction ID
|
|
350
|
+
* @param {Array<Buffer>} attributes - Attribute buffers
|
|
351
|
+
* @param {boolean} withIntegrity - Add MESSAGE-INTEGRITY
|
|
352
|
+
* @returns {Buffer} Complete STUN message
|
|
353
|
+
* @private
|
|
354
|
+
*/
|
|
355
|
+
_createMessage(messageType, transactionId, attributes, withIntegrity = false) {
|
|
356
|
+
let attributesBuffer = Buffer.concat(attributes);
|
|
357
|
+
|
|
358
|
+
// Add MESSAGE-INTEGRITY if needed
|
|
359
|
+
if (withIntegrity && this.credential) {
|
|
360
|
+
const tempHeader = Buffer.alloc(20);
|
|
361
|
+
tempHeader.writeUInt16BE(messageType, 0);
|
|
362
|
+
tempHeader.writeUInt16BE(attributesBuffer.length + 24, 2); // +24 for MESSAGE-INTEGRITY
|
|
363
|
+
tempHeader.writeUInt32BE(MAGIC_COOKIE, 4);
|
|
364
|
+
transactionId.copy(tempHeader, 8);
|
|
365
|
+
|
|
366
|
+
const tempMessage = Buffer.concat([tempHeader, attributesBuffer]);
|
|
367
|
+
|
|
368
|
+
// For TURN, compute key as MD5(username:realm:password) per RFC 5766
|
|
369
|
+
let key = this.credential;
|
|
370
|
+
if (this.username && this.realm) {
|
|
371
|
+
const keyString = `${this.username}:${this.realm}:${this.credential}`;
|
|
372
|
+
key = crypto.createHash('md5').update(keyString).digest();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const hmac = crypto.createHmac('sha1', key);
|
|
376
|
+
hmac.update(tempMessage);
|
|
377
|
+
const integrity = hmac.digest();
|
|
378
|
+
|
|
379
|
+
const integrityAttr = Buffer.alloc(4 + integrity.length);
|
|
380
|
+
integrityAttr.writeUInt16BE(STUN_ATTRIBUTES.MESSAGE_INTEGRITY, 0);
|
|
381
|
+
integrityAttr.writeUInt16BE(integrity.length, 2);
|
|
382
|
+
integrity.copy(integrityAttr, 4);
|
|
383
|
+
|
|
384
|
+
attributesBuffer = Buffer.concat([attributesBuffer, integrityAttr]);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Create final message
|
|
388
|
+
const header = Buffer.alloc(20);
|
|
389
|
+
header.writeUInt16BE(messageType, 0);
|
|
390
|
+
header.writeUInt16BE(attributesBuffer.length, 2);
|
|
391
|
+
header.writeUInt32BE(MAGIC_COOKIE, 4);
|
|
392
|
+
transactionId.copy(header, 8);
|
|
393
|
+
|
|
394
|
+
return Buffer.concat([header, attributesBuffer]);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Create a string attribute
|
|
399
|
+
* @param {number} type - Attribute type
|
|
400
|
+
* @param {string} value - String value
|
|
401
|
+
* @returns {Buffer} Attribute buffer
|
|
402
|
+
* @private
|
|
403
|
+
*/
|
|
404
|
+
_createStringAttribute(type, value) {
|
|
405
|
+
const valueBuffer = Buffer.from(value, 'utf8');
|
|
406
|
+
const length = valueBuffer.length;
|
|
407
|
+
const padding = (4 - (length % 4)) % 4;
|
|
408
|
+
const buffer = Buffer.alloc(4 + length + padding);
|
|
409
|
+
|
|
410
|
+
buffer.writeUInt16BE(type, 0);
|
|
411
|
+
buffer.writeUInt16BE(length, 2);
|
|
412
|
+
valueBuffer.copy(buffer, 4);
|
|
413
|
+
|
|
414
|
+
return buffer;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Handle incoming STUN message
|
|
419
|
+
* @param {Buffer} msg - Message buffer
|
|
420
|
+
* @param {Object} rinfo - Remote info
|
|
421
|
+
* @private
|
|
422
|
+
*/
|
|
423
|
+
_handleMessage(msg, rinfo) {
|
|
424
|
+
if (msg.length < 20) {
|
|
425
|
+
return; // Invalid STUN message
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const messageType = msg.readUInt16BE(0);
|
|
429
|
+
const messageLength = msg.readUInt16BE(2);
|
|
430
|
+
const magicCookie = msg.readUInt32BE(4);
|
|
431
|
+
const transactionId = msg.slice(8, 20);
|
|
432
|
+
|
|
433
|
+
if (magicCookie !== MAGIC_COOKIE) {
|
|
434
|
+
return; // Not a STUN message
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const transactionKey = transactionId.toString('hex');
|
|
438
|
+
const transaction = this.transactions.get(transactionKey);
|
|
439
|
+
|
|
440
|
+
if (!transaction) {
|
|
441
|
+
return; // Unknown transaction
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const attributes = this._parseAttributes(msg.slice(20, 20 + messageLength), transactionId);
|
|
445
|
+
|
|
446
|
+
// Handle STUN Binding responses
|
|
447
|
+
if (messageType === STUN_MESSAGE_TYPES.BINDING_RESPONSE) {
|
|
448
|
+
if (attributes.xorMappedAddress) {
|
|
449
|
+
transaction.resolve({
|
|
450
|
+
address: attributes.xorMappedAddress.address,
|
|
451
|
+
port: attributes.xorMappedAddress.port,
|
|
452
|
+
family: attributes.xorMappedAddress.family
|
|
453
|
+
});
|
|
454
|
+
} else if (attributes.mappedAddress) {
|
|
455
|
+
transaction.resolve({
|
|
456
|
+
address: attributes.mappedAddress.address,
|
|
457
|
+
port: attributes.mappedAddress.port,
|
|
458
|
+
family: attributes.mappedAddress.family
|
|
459
|
+
});
|
|
460
|
+
} else {
|
|
461
|
+
transaction.reject(new Error('No mapped address in STUN response'));
|
|
462
|
+
}
|
|
463
|
+
this.transactions.delete(transactionKey);
|
|
464
|
+
}
|
|
465
|
+
// Handle TURN Allocate responses
|
|
466
|
+
else if (messageType === STUN_MESSAGE_TYPES.ALLOCATE_RESPONSE) {
|
|
467
|
+
if (attributes.xorRelayedAddress) {
|
|
468
|
+
transaction.resolve({
|
|
469
|
+
relayedAddress: attributes.xorRelayedAddress.address,
|
|
470
|
+
relayedPort: attributes.xorRelayedAddress.port,
|
|
471
|
+
lifetime: attributes.lifetime || 600,
|
|
472
|
+
type: 'relay'
|
|
473
|
+
});
|
|
474
|
+
} else {
|
|
475
|
+
transaction.reject(new Error('No relayed address in ALLOCATE response'));
|
|
476
|
+
}
|
|
477
|
+
this.transactions.delete(transactionKey);
|
|
478
|
+
}
|
|
479
|
+
// Handle TURN Refresh responses
|
|
480
|
+
else if (messageType === STUN_MESSAGE_TYPES.REFRESH_RESPONSE) {
|
|
481
|
+
transaction.resolve({
|
|
482
|
+
lifetime: attributes.lifetime || 600
|
|
483
|
+
});
|
|
484
|
+
this.transactions.delete(transactionKey);
|
|
485
|
+
}
|
|
486
|
+
// Handle error responses
|
|
487
|
+
else if (messageType === STUN_MESSAGE_TYPES.BINDING_ERROR_RESPONSE ||
|
|
488
|
+
messageType === STUN_MESSAGE_TYPES.ALLOCATE_ERROR_RESPONSE) {
|
|
489
|
+
|
|
490
|
+
// Store realm and nonce for subsequent requests
|
|
491
|
+
if (attributes.realm) {
|
|
492
|
+
this.realm = attributes.realm;
|
|
493
|
+
}
|
|
494
|
+
if (attributes.nonce) {
|
|
495
|
+
this.nonce = attributes.nonce;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const errorMsg = attributes.errorCode || 'Unknown error';
|
|
499
|
+
transaction.reject(new Error(`STUN error: ${errorMsg}`));
|
|
500
|
+
this.transactions.delete(transactionKey);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Parse STUN attributes
|
|
506
|
+
* @param {Buffer} data - Attributes data
|
|
507
|
+
* @param {Buffer} transactionId - Transaction ID
|
|
508
|
+
* @returns {Object} Parsed attributes
|
|
509
|
+
* @private
|
|
510
|
+
*/
|
|
511
|
+
_parseAttributes(data, transactionId) {
|
|
512
|
+
const attributes = {};
|
|
513
|
+
let offset = 0;
|
|
514
|
+
|
|
515
|
+
while (offset < data.length) {
|
|
516
|
+
if (offset + 4 > data.length) break;
|
|
517
|
+
|
|
518
|
+
const type = data.readUInt16BE(offset);
|
|
519
|
+
const length = data.readUInt16BE(offset + 2);
|
|
520
|
+
offset += 4;
|
|
521
|
+
|
|
522
|
+
if (offset + length > data.length) break;
|
|
523
|
+
|
|
524
|
+
const value = data.slice(offset, offset + length);
|
|
525
|
+
|
|
526
|
+
switch (type) {
|
|
527
|
+
case STUN_ATTRIBUTES.XOR_MAPPED_ADDRESS:
|
|
528
|
+
attributes.xorMappedAddress = this._parseXorAddress(value, transactionId);
|
|
529
|
+
break;
|
|
530
|
+
case STUN_ATTRIBUTES.XOR_RELAYED_ADDRESS:
|
|
531
|
+
attributes.xorRelayedAddress = this._parseXorAddress(value, transactionId);
|
|
532
|
+
break;
|
|
533
|
+
case STUN_ATTRIBUTES.MAPPED_ADDRESS:
|
|
534
|
+
attributes.mappedAddress = this._parseAddress(value);
|
|
535
|
+
break;
|
|
536
|
+
case STUN_ATTRIBUTES.LIFETIME:
|
|
537
|
+
attributes.lifetime = value.readUInt32BE(0);
|
|
538
|
+
break;
|
|
539
|
+
case STUN_ATTRIBUTES.ERROR_CODE:
|
|
540
|
+
attributes.errorCode = this._parseErrorCode(value);
|
|
541
|
+
break;
|
|
542
|
+
case STUN_ATTRIBUTES.REALM:
|
|
543
|
+
attributes.realm = value.toString('utf8');
|
|
544
|
+
this.realm = attributes.realm;
|
|
545
|
+
break;
|
|
546
|
+
case STUN_ATTRIBUTES.NONCE:
|
|
547
|
+
attributes.nonce = value.toString('utf8');
|
|
548
|
+
this.nonce = attributes.nonce;
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Pad to 4-byte boundary
|
|
553
|
+
offset += length;
|
|
554
|
+
const padding = (4 - (length % 4)) % 4;
|
|
555
|
+
offset += padding;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return attributes;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Parse XOR-MAPPED-ADDRESS attribute
|
|
563
|
+
* @param {Buffer} data - Attribute data
|
|
564
|
+
* @param {Buffer} transactionId - Transaction ID
|
|
565
|
+
* @returns {Object} Address info
|
|
566
|
+
* @private
|
|
567
|
+
*/
|
|
568
|
+
_parseXorAddress(data, transactionId) {
|
|
569
|
+
const family = data.readUInt8(1);
|
|
570
|
+
const xorPort = data.readUInt16BE(2);
|
|
571
|
+
|
|
572
|
+
// XOR port with magic cookie high 16 bits
|
|
573
|
+
const port = xorPort ^ (MAGIC_COOKIE >> 16);
|
|
574
|
+
|
|
575
|
+
if (family === 0x01) { // IPv4
|
|
576
|
+
const xorAddress = data.readUInt32BE(4);
|
|
577
|
+
const address = xorAddress ^ MAGIC_COOKIE;
|
|
578
|
+
|
|
579
|
+
return {
|
|
580
|
+
family: 'IPv4',
|
|
581
|
+
port,
|
|
582
|
+
address: [
|
|
583
|
+
(address >> 24) & 0xFF,
|
|
584
|
+
(address >> 16) & 0xFF,
|
|
585
|
+
(address >> 8) & 0xFF,
|
|
586
|
+
address & 0xFF
|
|
587
|
+
].join('.')
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Parse MAPPED-ADDRESS attribute
|
|
596
|
+
* @param {Buffer} data - Attribute data
|
|
597
|
+
* @returns {Object} Address info
|
|
598
|
+
* @private
|
|
599
|
+
*/
|
|
600
|
+
_parseAddress(data) {
|
|
601
|
+
const family = data.readUInt8(1);
|
|
602
|
+
const port = data.readUInt16BE(2);
|
|
603
|
+
|
|
604
|
+
if (family === 0x01) { // IPv4
|
|
605
|
+
const address = data.slice(4, 8);
|
|
606
|
+
return {
|
|
607
|
+
family: 'IPv4',
|
|
608
|
+
port,
|
|
609
|
+
address: Array.from(address).join('.')
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Parse ERROR-CODE attribute
|
|
618
|
+
* @param {Buffer} data - Attribute data
|
|
619
|
+
* @returns {string} Error message
|
|
620
|
+
* @private
|
|
621
|
+
*/
|
|
622
|
+
_parseErrorCode(data) {
|
|
623
|
+
const errorClass = data.readUInt8(2) & 0x07;
|
|
624
|
+
const errorNumber = data.readUInt8(3);
|
|
625
|
+
const errorCode = errorClass * 100 + errorNumber;
|
|
626
|
+
const reason = data.slice(4).toString('utf8');
|
|
627
|
+
|
|
628
|
+
return `${errorCode} ${reason}`;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Close the client
|
|
633
|
+
*/
|
|
634
|
+
close() {
|
|
635
|
+
if (this.socket) {
|
|
636
|
+
this.socket.close();
|
|
637
|
+
this.socket = null;
|
|
638
|
+
}
|
|
639
|
+
this.transactions.clear();
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
module.exports = STUNClient;
|