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
package/src/TURNClient.js
DELETED
|
@@ -1,561 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TURN Client Implementation (RFC 5766)
|
|
3
|
-
* Pure Node.js implementation using dgram and net
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const dgram = require('dgram');
|
|
7
|
-
const net = require('net');
|
|
8
|
-
const crypto = require('crypto');
|
|
9
|
-
|
|
10
|
-
// TURN Message Types
|
|
11
|
-
const TURN_ALLOCATE_REQUEST = 0x0003;
|
|
12
|
-
const TURN_ALLOCATE_RESPONSE = 0x0103;
|
|
13
|
-
const TURN_ALLOCATE_ERROR = 0x0113;
|
|
14
|
-
const TURN_REFRESH_REQUEST = 0x0004;
|
|
15
|
-
const TURN_CREATE_PERMISSION = 0x0008;
|
|
16
|
-
const TURN_CHANNEL_BIND = 0x0009;
|
|
17
|
-
const TURN_SEND_INDICATION = 0x0016;
|
|
18
|
-
const TURN_DATA_INDICATION = 0x0017;
|
|
19
|
-
|
|
20
|
-
// TURN Attributes
|
|
21
|
-
const ATTR_XOR_RELAYED_ADDRESS = 0x0016;
|
|
22
|
-
const ATTR_XOR_MAPPED_ADDRESS = 0x0020;
|
|
23
|
-
const ATTR_LIFETIME = 0x000D;
|
|
24
|
-
const ATTR_USERNAME = 0x0006;
|
|
25
|
-
const ATTR_MESSAGE_INTEGRITY = 0x0008;
|
|
26
|
-
const ATTR_ERROR_CODE = 0x0009;
|
|
27
|
-
const ATTR_REALM = 0x0014;
|
|
28
|
-
const ATTR_NONCE = 0x0015;
|
|
29
|
-
const ATTR_XOR_PEER_ADDRESS = 0x0012;
|
|
30
|
-
const ATTR_DATA = 0x0013;
|
|
31
|
-
const ATTR_REQUESTED_TRANSPORT = 0x0019;
|
|
32
|
-
|
|
33
|
-
// STUN Magic Cookie
|
|
34
|
-
const MAGIC_COOKIE = 0x2112A442;
|
|
35
|
-
|
|
36
|
-
class TURNClient {
|
|
37
|
-
constructor(options = {}) {
|
|
38
|
-
this.server = options.server; // 'turn:server:port' or {host, port}
|
|
39
|
-
this.username = options.username || 'user';
|
|
40
|
-
this.password = options.password || 'pass';
|
|
41
|
-
this.transport = options.transport || 'udp'; // 'udp' or 'tcp'
|
|
42
|
-
this.socket = null;
|
|
43
|
-
this.timeout = options.timeout || 10000;
|
|
44
|
-
this.relayedAddress = null;
|
|
45
|
-
this.lifetime = 600; // Default 10 minutes
|
|
46
|
-
this.allocation = null;
|
|
47
|
-
|
|
48
|
-
// Authentication state for MESSAGE-INTEGRITY
|
|
49
|
-
this.realm = null;
|
|
50
|
-
this.nonce = null;
|
|
51
|
-
this.authenticated = false;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Parse TURN server URI
|
|
56
|
-
* @private
|
|
57
|
-
*/
|
|
58
|
-
_parseServer() {
|
|
59
|
-
if (typeof this.server === 'object') {
|
|
60
|
-
return this.server;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const match = this.server.match(/^turn:(.+):(\d+)$/);
|
|
64
|
-
if (match) {
|
|
65
|
-
return { host: match[1], port: parseInt(match[2], 10) };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Default TURN port
|
|
69
|
-
return { host: this.server, port: 3478 };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Allocate a relay address on TURN server
|
|
74
|
-
* @returns {Promise<{relayedAddress: string, relayedPort: number, mappedAddress: string, mappedPort: number}>}
|
|
75
|
-
*/
|
|
76
|
-
async allocate() {
|
|
77
|
-
const serverInfo = this._parseServer();
|
|
78
|
-
const transactionId = crypto.randomBytes(12);
|
|
79
|
-
|
|
80
|
-
return new Promise((resolve, reject) => {
|
|
81
|
-
let resolved = false;
|
|
82
|
-
|
|
83
|
-
const timeout = setTimeout(() => {
|
|
84
|
-
if (!resolved) {
|
|
85
|
-
resolved = true;
|
|
86
|
-
this.close();
|
|
87
|
-
reject(new Error('TURN allocation timeout'));
|
|
88
|
-
}
|
|
89
|
-
}, this.timeout);
|
|
90
|
-
|
|
91
|
-
// Create socket based on transport
|
|
92
|
-
if (this.transport === 'udp') {
|
|
93
|
-
this.socket = dgram.createSocket('udp4');
|
|
94
|
-
} else {
|
|
95
|
-
this.socket = new net.Socket();
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
this.socket.on('error', (err) => {
|
|
99
|
-
if (!resolved) {
|
|
100
|
-
resolved = true;
|
|
101
|
-
clearTimeout(timeout);
|
|
102
|
-
this.close();
|
|
103
|
-
reject(err);
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
const handleMessage = (msg) => {
|
|
108
|
-
if (resolved) return;
|
|
109
|
-
|
|
110
|
-
try {
|
|
111
|
-
const result = this._parseAllocateResponse(msg);
|
|
112
|
-
if (result) {
|
|
113
|
-
resolved = true;
|
|
114
|
-
clearTimeout(timeout);
|
|
115
|
-
this.relayedAddress = result.relayedAddress;
|
|
116
|
-
this.allocation = result;
|
|
117
|
-
resolve(result);
|
|
118
|
-
}
|
|
119
|
-
} catch (err) {
|
|
120
|
-
// Check if this is a 401 Unauthorized requiring authentication
|
|
121
|
-
if (err.message.includes('401') && !this.authenticated && this.realm && this.nonce) {
|
|
122
|
-
// Clear the error handler and retry with authentication
|
|
123
|
-
clearTimeout(timeout);
|
|
124
|
-
this._retryAllocationWithAuth(serverInfo, transactionId)
|
|
125
|
-
.then(result => {
|
|
126
|
-
resolved = true;
|
|
127
|
-
resolve(result);
|
|
128
|
-
})
|
|
129
|
-
.catch(authErr => {
|
|
130
|
-
resolved = true;
|
|
131
|
-
this.close();
|
|
132
|
-
reject(authErr);
|
|
133
|
-
});
|
|
134
|
-
} else {
|
|
135
|
-
resolved = true;
|
|
136
|
-
clearTimeout(timeout);
|
|
137
|
-
this.close();
|
|
138
|
-
reject(err);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
if (this.transport === 'udp') {
|
|
144
|
-
this.socket.on('message', handleMessage);
|
|
145
|
-
} else {
|
|
146
|
-
this.socket.on('data', handleMessage);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Connect and send allocation request
|
|
150
|
-
const sendRequest = () => {
|
|
151
|
-
const request = this._createAllocateRequest(transactionId);
|
|
152
|
-
|
|
153
|
-
if (this.transport === 'udp') {
|
|
154
|
-
this.socket.send(request, serverInfo.port, serverInfo.host, (err) => {
|
|
155
|
-
if (err && !resolved) {
|
|
156
|
-
resolved = true;
|
|
157
|
-
clearTimeout(timeout);
|
|
158
|
-
this.close();
|
|
159
|
-
reject(err);
|
|
160
|
-
}
|
|
161
|
-
});
|
|
162
|
-
} else {
|
|
163
|
-
this.socket.write(request);
|
|
164
|
-
}
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
if (this.transport === 'tcp') {
|
|
168
|
-
this.socket.connect(serverInfo.port, serverInfo.host, sendRequest);
|
|
169
|
-
} else {
|
|
170
|
-
sendRequest();
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Create TURN Allocate Request
|
|
177
|
-
* @private
|
|
178
|
-
*/
|
|
179
|
-
_createAllocateRequest(transactionId, withAuth = false) {
|
|
180
|
-
const attributes = [];
|
|
181
|
-
|
|
182
|
-
// REQUESTED-TRANSPORT attribute (UDP = 17)
|
|
183
|
-
const transportAttr = Buffer.allocUnsafe(8);
|
|
184
|
-
transportAttr.writeUInt16BE(ATTR_REQUESTED_TRANSPORT, 0);
|
|
185
|
-
transportAttr.writeUInt16BE(4, 2);
|
|
186
|
-
transportAttr.writeUInt8(17, 4); // UDP protocol
|
|
187
|
-
transportAttr.writeUInt8(0, 5);
|
|
188
|
-
transportAttr.writeUInt8(0, 6);
|
|
189
|
-
transportAttr.writeUInt8(0, 7);
|
|
190
|
-
attributes.push(transportAttr);
|
|
191
|
-
|
|
192
|
-
// Add authentication attributes if needed
|
|
193
|
-
if (withAuth && this.username && this.realm && this.nonce) {
|
|
194
|
-
// USERNAME attribute
|
|
195
|
-
const usernameAttr = this._createStringAttribute(0x0006, this.username);
|
|
196
|
-
attributes.push(usernameAttr);
|
|
197
|
-
|
|
198
|
-
// REALM attribute
|
|
199
|
-
const realmAttr = this._createStringAttribute(0x0014, this.realm);
|
|
200
|
-
attributes.push(realmAttr);
|
|
201
|
-
|
|
202
|
-
// NONCE attribute
|
|
203
|
-
const nonceAttr = this._createStringAttribute(0x0015, this.nonce);
|
|
204
|
-
attributes.push(nonceAttr);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Calculate total attributes length (before MESSAGE-INTEGRITY)
|
|
208
|
-
let attrLength = 0;
|
|
209
|
-
for (const attr of attributes) {
|
|
210
|
-
attrLength += attr.length;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// If using auth, add MESSAGE-INTEGRITY (will be added after header)
|
|
214
|
-
let messageIntegrityAttr = null;
|
|
215
|
-
if (withAuth && this.username && this.realm && this.nonce && this.password) {
|
|
216
|
-
attrLength += 24; // MESSAGE-INTEGRITY attribute size (4 + 20)
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// STUN header
|
|
220
|
-
const header = Buffer.allocUnsafe(20);
|
|
221
|
-
header.writeUInt16BE(TURN_ALLOCATE_REQUEST, 0);
|
|
222
|
-
header.writeUInt16BE(attrLength, 2);
|
|
223
|
-
header.writeUInt32BE(MAGIC_COOKIE, 4);
|
|
224
|
-
transactionId.copy(header, 8);
|
|
225
|
-
|
|
226
|
-
// Combine header and attributes
|
|
227
|
-
let message = Buffer.concat([header, ...attributes]);
|
|
228
|
-
|
|
229
|
-
// Add MESSAGE-INTEGRITY if using auth
|
|
230
|
-
if (withAuth && this.username && this.realm && this.nonce && this.password) {
|
|
231
|
-
messageIntegrityAttr = this._createMessageIntegrity(message);
|
|
232
|
-
message = Buffer.concat([message, messageIntegrityAttr]);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
return message;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Parse TURN Allocate Response
|
|
240
|
-
* @private
|
|
241
|
-
*/
|
|
242
|
-
_parseAllocateResponse(msg) {
|
|
243
|
-
if (msg.length < 20) {
|
|
244
|
-
throw new Error('Invalid TURN response: too short');
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const messageType = msg.readUInt16BE(0);
|
|
248
|
-
const messageLength = msg.readUInt16BE(2);
|
|
249
|
-
const magicCookie = msg.readUInt32BE(4);
|
|
250
|
-
|
|
251
|
-
// Check if this is an allocate response
|
|
252
|
-
if (messageType === TURN_ALLOCATE_ERROR) {
|
|
253
|
-
const error = this._parseErrorCode(msg);
|
|
254
|
-
|
|
255
|
-
// If 401 Unauthorized, extract REALM and NONCE for retry
|
|
256
|
-
if (error.includes('401')) {
|
|
257
|
-
this._extractAuthAttributes(msg);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
throw new Error(`TURN allocation failed: ${error}`);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
if (messageType !== TURN_ALLOCATE_RESPONSE) {
|
|
264
|
-
return null;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Verify magic cookie
|
|
268
|
-
if (magicCookie !== MAGIC_COOKIE) {
|
|
269
|
-
throw new Error('Invalid TURN response: bad magic cookie');
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// Parse attributes
|
|
273
|
-
const result = {};
|
|
274
|
-
let offset = 20;
|
|
275
|
-
|
|
276
|
-
while (offset < 20 + messageLength) {
|
|
277
|
-
const attrType = msg.readUInt16BE(offset);
|
|
278
|
-
const attrLength = msg.readUInt16BE(offset + 2);
|
|
279
|
-
const attrValue = msg.slice(offset + 4, offset + 4 + attrLength);
|
|
280
|
-
|
|
281
|
-
if (attrType === ATTR_XOR_RELAYED_ADDRESS) {
|
|
282
|
-
const addr = this._parseXorAddress(attrValue, msg.slice(8, 20));
|
|
283
|
-
result.relayedAddress = addr.ip;
|
|
284
|
-
result.relayedPort = addr.port;
|
|
285
|
-
result.type = 'relay';
|
|
286
|
-
} else if (attrType === ATTR_XOR_MAPPED_ADDRESS) {
|
|
287
|
-
const addr = this._parseXorAddress(attrValue, msg.slice(8, 20));
|
|
288
|
-
result.mappedAddress = addr.ip;
|
|
289
|
-
result.mappedPort = addr.port;
|
|
290
|
-
} else if (attrType === ATTR_LIFETIME) {
|
|
291
|
-
this.lifetime = attrValue.readUInt32BE(0);
|
|
292
|
-
result.lifetime = this.lifetime;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Move to next attribute (with padding)
|
|
296
|
-
offset += 4 + attrLength;
|
|
297
|
-
if (attrLength % 4 !== 0) {
|
|
298
|
-
offset += 4 - (attrLength % 4);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
if (!result.relayedAddress) {
|
|
303
|
-
throw new Error('No relayed address in TURN response');
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
return result;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Parse XOR address attribute
|
|
311
|
-
* @private
|
|
312
|
-
*/
|
|
313
|
-
_parseXorAddress(value, transactionId) {
|
|
314
|
-
const family = value.readUInt8(1);
|
|
315
|
-
const xPort = value.readUInt16BE(2);
|
|
316
|
-
const xAddress = value.slice(4, 8);
|
|
317
|
-
|
|
318
|
-
// XOR with magic cookie
|
|
319
|
-
const port = xPort ^ (MAGIC_COOKIE >> 16);
|
|
320
|
-
|
|
321
|
-
const addressBytes = Buffer.allocUnsafe(4);
|
|
322
|
-
const magicBytes = Buffer.allocUnsafe(4);
|
|
323
|
-
magicBytes.writeUInt32BE(MAGIC_COOKIE, 0);
|
|
324
|
-
|
|
325
|
-
for (let i = 0; i < 4; i++) {
|
|
326
|
-
addressBytes[i] = xAddress[i] ^ magicBytes[i];
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const ip = Array.from(addressBytes).join('.');
|
|
330
|
-
|
|
331
|
-
return { ip, port };
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* Parse error code attribute
|
|
336
|
-
* @private
|
|
337
|
-
*/
|
|
338
|
-
_parseErrorCode(msg) {
|
|
339
|
-
let offset = 20;
|
|
340
|
-
const messageLength = msg.readUInt16BE(2);
|
|
341
|
-
|
|
342
|
-
while (offset < 20 + messageLength) {
|
|
343
|
-
const attrType = msg.readUInt16BE(offset);
|
|
344
|
-
const attrLength = msg.readUInt16BE(offset + 2);
|
|
345
|
-
const attrValue = msg.slice(offset + 4, offset + 4 + attrLength);
|
|
346
|
-
|
|
347
|
-
if (attrType === ATTR_ERROR_CODE) {
|
|
348
|
-
const errorClass = attrValue.readUInt8(2);
|
|
349
|
-
const errorNumber = attrValue.readUInt8(3);
|
|
350
|
-
const errorCode = errorClass * 100 + errorNumber;
|
|
351
|
-
const errorText = attrValue.slice(4).toString('utf8');
|
|
352
|
-
return `${errorCode} ${errorText}`;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
offset += 4 + attrLength;
|
|
356
|
-
if (attrLength % 4 !== 0) {
|
|
357
|
-
offset += 4 - (attrLength % 4);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
return 'Unknown error';
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/**
|
|
365
|
-
* Refresh the allocation
|
|
366
|
-
* @param {number} lifetime - New lifetime in seconds
|
|
367
|
-
*/
|
|
368
|
-
async refresh(lifetime = 600) {
|
|
369
|
-
if (!this.allocation) {
|
|
370
|
-
throw new Error('No active allocation');
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// Send refresh request
|
|
374
|
-
// Implementation similar to allocate()
|
|
375
|
-
this.lifetime = lifetime;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
* Send data through TURN relay
|
|
380
|
-
* @param {Buffer} data - Data to send
|
|
381
|
-
* @param {string} peerAddress - Peer IP address
|
|
382
|
-
* @param {number} peerPort - Peer port
|
|
383
|
-
*/
|
|
384
|
-
async send(data, peerAddress, peerPort) {
|
|
385
|
-
if (!this.allocation) {
|
|
386
|
-
throw new Error('No active allocation');
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// Create Send Indication message
|
|
390
|
-
const transactionId = crypto.randomBytes(12);
|
|
391
|
-
const message = this._createSendIndication(data, peerAddress, peerPort, transactionId);
|
|
392
|
-
|
|
393
|
-
const serverInfo = this._parseServer();
|
|
394
|
-
|
|
395
|
-
if (this.transport === 'udp') {
|
|
396
|
-
this.socket.send(message, serverInfo.port, serverInfo.host);
|
|
397
|
-
} else {
|
|
398
|
-
this.socket.write(message);
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/**
|
|
403
|
-
* Create Send Indication message
|
|
404
|
-
* @private
|
|
405
|
-
*/
|
|
406
|
-
_createSendIndication(data, peerAddress, peerPort, transactionId) {
|
|
407
|
-
// Implementation of TURN Send Indication
|
|
408
|
-
// For now, simplified version
|
|
409
|
-
const header = Buffer.allocUnsafe(20);
|
|
410
|
-
header.writeUInt16BE(TURN_SEND_INDICATION, 0);
|
|
411
|
-
header.writeUInt16BE(data.length, 2);
|
|
412
|
-
header.writeUInt32BE(MAGIC_COOKIE, 4);
|
|
413
|
-
transactionId.copy(header, 8);
|
|
414
|
-
|
|
415
|
-
return Buffer.concat([header, data]);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
/**
|
|
419
|
-
* Close the TURN client
|
|
420
|
-
*/
|
|
421
|
-
close() {
|
|
422
|
-
if (this.socket) {
|
|
423
|
-
if (this.transport === 'udp') {
|
|
424
|
-
this.socket.close();
|
|
425
|
-
} else {
|
|
426
|
-
this.socket.destroy();
|
|
427
|
-
}
|
|
428
|
-
this.socket = null;
|
|
429
|
-
}
|
|
430
|
-
this.allocation = null;
|
|
431
|
-
this.relayedAddress = null;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
/**
|
|
435
|
-
* Extract authentication attributes (REALM, NONCE) from error response
|
|
436
|
-
* @private
|
|
437
|
-
*/
|
|
438
|
-
_extractAuthAttributes(msg) {
|
|
439
|
-
let offset = 20;
|
|
440
|
-
const messageLength = msg.readUInt16BE(2);
|
|
441
|
-
|
|
442
|
-
while (offset < 20 + messageLength) {
|
|
443
|
-
const attrType = msg.readUInt16BE(offset);
|
|
444
|
-
const attrLength = msg.readUInt16BE(offset + 2);
|
|
445
|
-
|
|
446
|
-
if (attrType === 0x0014) { // REALM
|
|
447
|
-
this.realm = msg.slice(offset + 4, offset + 4 + attrLength).toString('utf8');
|
|
448
|
-
} else if (attrType === 0x0015) { // NONCE
|
|
449
|
-
this.nonce = msg.slice(offset + 4, offset + 4 + attrLength).toString('utf8');
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
offset += 4 + attrLength;
|
|
453
|
-
const padding = (4 - (attrLength % 4)) % 4;
|
|
454
|
-
offset += padding;
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
/**
|
|
459
|
-
* Create a string attribute (USERNAME, REALM, NONCE)
|
|
460
|
-
* @private
|
|
461
|
-
*/
|
|
462
|
-
_createStringAttribute(type, value) {
|
|
463
|
-
const valueBuffer = Buffer.from(value, 'utf8');
|
|
464
|
-
const length = valueBuffer.length;
|
|
465
|
-
const padding = (4 - (length % 4)) % 4;
|
|
466
|
-
|
|
467
|
-
const attr = Buffer.alloc(4 + length + padding);
|
|
468
|
-
attr.writeUInt16BE(type, 0);
|
|
469
|
-
attr.writeUInt16BE(length, 2);
|
|
470
|
-
valueBuffer.copy(attr, 4);
|
|
471
|
-
|
|
472
|
-
return attr;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
/**
|
|
476
|
-
* Create MESSAGE-INTEGRITY attribute (RFC 5766 Section 15.4)
|
|
477
|
-
* @private
|
|
478
|
-
*/
|
|
479
|
-
_createMessageIntegrity(message) {
|
|
480
|
-
// Compute key = MD5(username:realm:password)
|
|
481
|
-
const keyString = `${this.username}:${this.realm}:${this.password}`;
|
|
482
|
-
const key = crypto.createHash('md5').update(keyString).digest();
|
|
483
|
-
|
|
484
|
-
// Compute HMAC-SHA1 of the message
|
|
485
|
-
const hmac = crypto.createHmac('sha1', key).update(message).digest();
|
|
486
|
-
|
|
487
|
-
// Create MESSAGE-INTEGRITY attribute (type 0x0008)
|
|
488
|
-
const attr = Buffer.alloc(24);
|
|
489
|
-
attr.writeUInt16BE(0x0008, 0); // MESSAGE-INTEGRITY
|
|
490
|
-
attr.writeUInt16BE(20, 2); // SHA1 hash is 20 bytes
|
|
491
|
-
hmac.copy(attr, 4);
|
|
492
|
-
|
|
493
|
-
return attr;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
/**
|
|
497
|
-
* Retry allocation with authentication after 401
|
|
498
|
-
* @private
|
|
499
|
-
*/
|
|
500
|
-
_retryAllocationWithAuth(serverInfo, transactionId) {
|
|
501
|
-
return new Promise((resolve, reject) => {
|
|
502
|
-
this.authenticated = true;
|
|
503
|
-
|
|
504
|
-
const request = this._createAllocateRequest(transactionId, true);
|
|
505
|
-
|
|
506
|
-
let resolved = false;
|
|
507
|
-
const timeout = setTimeout(() => {
|
|
508
|
-
if (!resolved) {
|
|
509
|
-
resolved = true;
|
|
510
|
-
this.close();
|
|
511
|
-
reject(new Error('TURN authenticated allocation timeout'));
|
|
512
|
-
}
|
|
513
|
-
}, this.timeout);
|
|
514
|
-
|
|
515
|
-
const handleAuthMessage = (msg) => {
|
|
516
|
-
if (resolved) return;
|
|
517
|
-
|
|
518
|
-
try {
|
|
519
|
-
const result = this._parseAllocateResponse(msg);
|
|
520
|
-
if (result) {
|
|
521
|
-
resolved = true;
|
|
522
|
-
clearTimeout(timeout);
|
|
523
|
-
if (this.socket) {
|
|
524
|
-
this.socket.removeListener('message', handleAuthMessage);
|
|
525
|
-
this.socket.removeListener('data', handleAuthMessage);
|
|
526
|
-
}
|
|
527
|
-
this.relayedAddress = result.relayedAddress;
|
|
528
|
-
this.allocation = result;
|
|
529
|
-
resolve(result);
|
|
530
|
-
}
|
|
531
|
-
} catch (err) {
|
|
532
|
-
resolved = true;
|
|
533
|
-
clearTimeout(timeout);
|
|
534
|
-
if (this.socket) {
|
|
535
|
-
this.socket.removeListener('message', handleAuthMessage);
|
|
536
|
-
this.socket.removeListener('data', handleAuthMessage);
|
|
537
|
-
}
|
|
538
|
-
this.close();
|
|
539
|
-
reject(err);
|
|
540
|
-
}
|
|
541
|
-
};
|
|
542
|
-
|
|
543
|
-
if (this.transport === 'udp') {
|
|
544
|
-
this.socket.on('message', handleAuthMessage);
|
|
545
|
-
this.socket.send(request, serverInfo.port, serverInfo.host, (err) => {
|
|
546
|
-
if (err && !resolved) {
|
|
547
|
-
resolved = true;
|
|
548
|
-
clearTimeout(timeout);
|
|
549
|
-
this.close();
|
|
550
|
-
reject(err);
|
|
551
|
-
}
|
|
552
|
-
});
|
|
553
|
-
} else {
|
|
554
|
-
this.socket.on('data', handleAuthMessage);
|
|
555
|
-
this.socket.write(request);
|
|
556
|
-
}
|
|
557
|
-
});
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
module.exports = TURNClient;
|