node-rtc-connection 1.0.19 → 2.0.5

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