node-rtc-connection 1.0.11 → 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/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;