node-rtc-connection 1.0.18 → 2.0.4

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 (65) hide show
  1. package/README.md +94 -85
  2. package/dist/index.cjs +20 -5421
  3. package/dist/index.mjs +25 -5413
  4. package/dist/types/crypto/der.d.ts +107 -0
  5. package/dist/types/crypto/x509.d.ts +56 -0
  6. package/dist/types/datachannel/RTCDataChannel.d.ts +179 -0
  7. package/dist/types/dtls/RTCCertificate.d.ts +163 -0
  8. package/dist/types/dtls/cipher.d.ts +81 -0
  9. package/dist/types/dtls/connection.d.ts +81 -0
  10. package/dist/types/dtls/prf.d.ts +29 -0
  11. package/dist/types/dtls/protocol.d.ts +127 -0
  12. package/dist/types/foundation/ByteBufferQueue.d.ts +71 -0
  13. package/dist/types/foundation/RTCError.d.ts +152 -0
  14. package/dist/types/ice/RTCIceCandidate.d.ts +161 -0
  15. package/dist/types/ice/ice-agent.d.ts +154 -0
  16. package/dist/types/ice/stun-message.d.ts +92 -0
  17. package/dist/types/index.d.ts +29 -0
  18. package/dist/types/peerconnection/RTCPeerConnection.d.ts +74 -0
  19. package/dist/types/sctp/association.d.ts +77 -0
  20. package/dist/types/sctp/chunks.d.ts +200 -0
  21. package/dist/types/sctp/crc32c.d.ts +24 -0
  22. package/dist/types/sctp/datachannel-manager.d.ts +51 -0
  23. package/dist/types/sctp/dcep.d.ts +56 -0
  24. package/dist/types/sdp/RTCSessionDescription.d.ts +73 -0
  25. package/dist/types/sdp/sdp-utils.d.ts +103 -0
  26. package/dist/types/stun/stun-client.d.ts +119 -0
  27. package/dist/types/transport-stack.d.ts +68 -0
  28. package/package.json +26 -21
  29. package/src/crypto/der.ts +205 -0
  30. package/src/crypto/x509.ts +146 -0
  31. package/src/datachannel/RTCDataChannel.ts +388 -0
  32. package/src/dtls/RTCCertificate.ts +396 -0
  33. package/src/dtls/cipher.ts +198 -0
  34. package/src/dtls/connection.ts +974 -0
  35. package/src/dtls/prf.ts +62 -0
  36. package/src/dtls/protocol.ts +204 -0
  37. package/src/foundation/{ByteBufferQueue.js → ByteBufferQueue.ts} +74 -72
  38. package/src/foundation/{RTCError.js → RTCError.ts} +110 -60
  39. package/src/ice/{RTCIceCandidate.js → RTCIceCandidate.ts} +140 -92
  40. package/src/ice/ice-agent.ts +609 -0
  41. package/src/ice/stun-message.ts +260 -0
  42. package/src/index.ts +72 -0
  43. package/src/peerconnection/RTCPeerConnection.ts +430 -0
  44. package/src/sctp/association.ts +523 -0
  45. package/src/sctp/chunks.ts +350 -0
  46. package/src/sctp/crc32c.ts +57 -0
  47. package/src/sctp/datachannel-manager.ts +187 -0
  48. package/src/sctp/dcep.ts +94 -0
  49. package/src/sdp/{RTCSessionDescription.js → RTCSessionDescription.ts} +42 -29
  50. package/src/sdp/sdp-utils.ts +229 -0
  51. package/src/stun/stun-client.ts +936 -0
  52. package/src/transport-stack.ts +165 -0
  53. package/dist/index.cjs.map +0 -1
  54. package/dist/index.mjs.map +0 -1
  55. package/src/datachannel/RTCDataChannel.js +0 -354
  56. package/src/dtls/RTCCertificate.js +0 -310
  57. package/src/dtls/RTCDtlsTransport.js +0 -247
  58. package/src/ice/RTCIceTransport.js +0 -998
  59. package/src/index.d.ts +0 -400
  60. package/src/index.js +0 -92
  61. package/src/network/network-transport.js +0 -478
  62. package/src/peerconnection/RTCPeerConnection.js +0 -851
  63. package/src/sctp/RTCSctpTransport.js +0 -253
  64. package/src/sdp/sdp-utils.js +0 -224
  65. package/src/stun/stun-client.js +0 -643
@@ -0,0 +1,936 @@
1
+ /**
2
+ * @file stun-client.ts
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
+ import * as dgram from 'dgram';
13
+ import * as crypto from 'crypto';
14
+
15
+ import { EventEmitter } from '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
+ } as const;
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
+ } as const;
67
+
68
+ const MAGIC_COOKIE = 0x2112a442;
69
+
70
+ /**
71
+ * Constructor options for {@link STUNClient}.
72
+ */
73
+ interface STUNClientOptions {
74
+ /** STUN/TURN server address */
75
+ server: string;
76
+ /** Server port */
77
+ port: number;
78
+ /** TURN username */
79
+ username?: string;
80
+ /** TURN password */
81
+ credential?: string;
82
+ /** Transport protocol (udp/tcp) */
83
+ transport?: string;
84
+ /** Additional query parameters from URL */
85
+ params?: Record<string, unknown>;
86
+ }
87
+
88
+ /**
89
+ * Parsed IPv4 address info.
90
+ */
91
+ interface AddressInfo {
92
+ family: string;
93
+ port: number;
94
+ address: string;
95
+ }
96
+
97
+ /**
98
+ * Reflexive address info resolved from a STUN Binding response.
99
+ */
100
+ interface ReflexiveAddress {
101
+ address: string;
102
+ port: number;
103
+ family: string;
104
+ }
105
+
106
+ /**
107
+ * Relay address info resolved from a TURN Allocate response.
108
+ */
109
+ interface RelayAddress {
110
+ relayedAddress: string;
111
+ relayedPort: number;
112
+ lifetime: number;
113
+ type: 'relay';
114
+ }
115
+
116
+ /**
117
+ * Result of a TURN Refresh response.
118
+ */
119
+ interface RefreshResult {
120
+ lifetime: number;
121
+ }
122
+
123
+ /**
124
+ * Result of a generic success response (CreatePermission / ChannelBind).
125
+ */
126
+ interface OkResult {
127
+ ok: true;
128
+ }
129
+
130
+ /**
131
+ * Union of every result shape a transaction may resolve with.
132
+ */
133
+ type TransactionResult = ReflexiveAddress | RelayAddress | RefreshResult | OkResult;
134
+
135
+ /**
136
+ * A pending request transaction.
137
+ */
138
+ interface Transaction {
139
+ type?: string;
140
+ resolve: (result: TransactionResult) => void;
141
+ reject: (error: Error) => void;
142
+ }
143
+
144
+ /**
145
+ * Parsed STUN attributes object.
146
+ */
147
+ interface ParsedAttributes {
148
+ xorMappedAddress?: AddressInfo | null;
149
+ xorRelayedAddress?: AddressInfo | null;
150
+ xorPeerAddress?: AddressInfo | null;
151
+ mappedAddress?: AddressInfo | null;
152
+ data?: Buffer;
153
+ lifetime?: number;
154
+ errorCode?: string;
155
+ realm?: string;
156
+ nonce?: string;
157
+ }
158
+
159
+ /**
160
+ * Payload emitted with the 'data' event for relayed peer data.
161
+ */
162
+ interface DataEventInfo {
163
+ address: string;
164
+ port: number;
165
+ family: string;
166
+ }
167
+
168
+ /**
169
+ * Build product passed back from a request builder used with auth retry.
170
+ */
171
+ interface RequestBuild {
172
+ transactionId: Buffer;
173
+ request: Buffer;
174
+ }
175
+
176
+ /**
177
+ * @class STUNClient
178
+ * @description STUN/TURN client for NAT traversal
179
+ */
180
+ class STUNClient extends EventEmitter {
181
+ #server: string;
182
+ #port: number;
183
+ #username: string | undefined;
184
+ #credential: string | undefined;
185
+ #socket: dgram.Socket | null;
186
+ #transactions: Map<string, Transaction>;
187
+ #realm: string | null;
188
+ #nonce: string | null;
189
+
190
+ /**
191
+ * Create a STUN client
192
+ * @param {Object} options - Client options
193
+ * @param {string} options.server - STUN/TURN server address
194
+ * @param {number} options.port - Server port
195
+ * @param {string} [options.username] - TURN username
196
+ * @param {string} [options.credential] - TURN password
197
+ * @param {string} [options.transport='udp'] - Transport protocol (udp/tcp)
198
+ * @param {Object} [options.params={}] - Additional query parameters from URL
199
+ */
200
+ constructor(options: STUNClientOptions) {
201
+ super();
202
+ this.#server = options.server;
203
+ this.#port = options.port;
204
+ this.#username = options.username;
205
+ this.#credential = options.credential;
206
+
207
+ this.#socket = null;
208
+ this.#transactions = new Map();
209
+ this.#realm = null;
210
+ this.#nonce = null;
211
+ }
212
+
213
+ /**
214
+ * Connect to the STUN/TURN server
215
+ * @returns {Promise<void>}
216
+ */
217
+ async connect(): Promise<void> {
218
+ if (this.#socket) {
219
+ return;
220
+ }
221
+
222
+ return new Promise<void>((resolve, reject) => {
223
+ const socket = dgram.createSocket('udp4');
224
+ this.#socket = socket;
225
+
226
+ socket.on('message', (msg: Buffer, rinfo: dgram.RemoteInfo) => {
227
+ this.#handleMessage(msg, rinfo);
228
+ });
229
+
230
+ socket.on('error', (err: Error) => {
231
+ console.error('STUN socket error:', err);
232
+ reject(err);
233
+ });
234
+
235
+ socket.bind(() => {
236
+ resolve();
237
+ });
238
+ });
239
+ }
240
+
241
+ /**
242
+ * Send a STUN Binding Request to get reflexive address
243
+ * @returns {Promise<Object>} Reflexive address info
244
+ */
245
+ async getReflexiveAddress(): Promise<TransactionResult> {
246
+ await this.connect();
247
+
248
+ const transactionId = crypto.randomBytes(12);
249
+ const request = this.#createBindingRequest(transactionId);
250
+
251
+ return new Promise<TransactionResult>((resolve, reject) => {
252
+ const timeout = setTimeout(() => {
253
+ this.#transactions.delete(transactionId.toString('hex'));
254
+ reject(new Error('STUN request timeout'));
255
+ }, 5000);
256
+
257
+ this.#transactions.set(transactionId.toString('hex'), {
258
+ resolve: (result: TransactionResult) => {
259
+ clearTimeout(timeout);
260
+ resolve(result);
261
+ },
262
+ reject: (error: Error) => {
263
+ clearTimeout(timeout);
264
+ reject(error);
265
+ },
266
+ });
267
+
268
+ this.#socket!.send(request, this.#port, this.#server, (err) => {
269
+ if (err) {
270
+ clearTimeout(timeout);
271
+ this.#transactions.delete(transactionId.toString('hex'));
272
+ reject(err);
273
+ }
274
+ });
275
+ });
276
+ }
277
+
278
+ /**
279
+ * Send a TURN Allocate Request to get relay address
280
+ * @param {number} [lifetime=600] - Allocation lifetime in seconds
281
+ * @returns {Promise<Object>} Relay address info
282
+ */
283
+ async allocateRelay(lifetime: number = 600): Promise<TransactionResult> {
284
+ if (!this.#username || !this.#credential) {
285
+ throw new Error('TURN requires username and credential');
286
+ }
287
+
288
+ await this.connect();
289
+
290
+ let transactionId = crypto.randomBytes(12);
291
+ let request = this.#createAllocateRequest(transactionId, lifetime);
292
+
293
+ // First attempt without credentials to get realm and nonce
294
+ try {
295
+ return await this.#sendRequest(request, transactionId, 'allocate');
296
+ } catch (error) {
297
+ // If we get 401 Unauthorized, retry with credentials
298
+ if (error instanceof Error && error.message.includes('401') && this.#realm && this.#nonce) {
299
+ // Create new transaction ID for retry
300
+ transactionId = crypto.randomBytes(12);
301
+ request = this.#createAllocateRequest(transactionId, lifetime, true);
302
+ return await this.#sendRequest(request, transactionId, 'allocate');
303
+ }
304
+ throw error;
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Send a TURN Refresh Request to keep allocation alive
310
+ * @param {number} [lifetime=600] - Allocation lifetime in seconds
311
+ * @returns {Promise<Object>} Updated allocation info
312
+ */
313
+ async refreshAllocation(lifetime: number = 600): Promise<TransactionResult> {
314
+ if (!this.#username || !this.#credential) {
315
+ throw new Error('TURN requires username and credential');
316
+ }
317
+
318
+ return this.#withAuthRetry('refresh', () => {
319
+ const transactionId = crypto.randomBytes(12);
320
+ return { transactionId, request: this.#createRefreshRequest(transactionId, lifetime) };
321
+ });
322
+ }
323
+
324
+ /**
325
+ * Send an authenticated TURN request, retrying once on a 401 (stale-nonce or
326
+ * first-time challenge) after refreshing realm/nonce from the error.
327
+ * @param {string} type - request label for diagnostics
328
+ * @param {() => {transactionId: Buffer, request: Buffer}} build
329
+ * @returns {Promise<Object>}
330
+ * @private
331
+ */
332
+ async #withAuthRetry(type: string, build: () => RequestBuild): Promise<TransactionResult> {
333
+ const first = build();
334
+ try {
335
+ return await this.#sendRequest(first.request, first.transactionId, type);
336
+ } catch (error) {
337
+ if (error instanceof Error && error.message.includes('401') && this.#realm && this.#nonce) {
338
+ const retry = build(); // rebuilt with the refreshed realm/nonce
339
+ return this.#sendRequest(retry.request, retry.transactionId, type);
340
+ }
341
+ throw error;
342
+ }
343
+ }
344
+
345
+ /**
346
+ * Create a TURN Permission for a peer
347
+ * @param {string} peerAddress - Peer IP address
348
+ * @returns {Promise<void>}
349
+ */
350
+ async createPermission(peerAddress: string): Promise<void> {
351
+ if (!this.#username || !this.#credential) {
352
+ throw new Error('TURN requires username and credential');
353
+ }
354
+
355
+ await this.#withAuthRetry('createPermission', () => {
356
+ const transactionId = crypto.randomBytes(12);
357
+ return { transactionId, request: this.#createCreatePermissionRequest(transactionId, peerAddress) };
358
+ });
359
+ }
360
+
361
+ /**
362
+ * Send data to a peer via TURN Send Indication
363
+ * @param {string} peerAddress - Peer IP address
364
+ * @param {number} peerPort - Peer port
365
+ * @param {Buffer} data - Data to send
366
+ * @returns {Promise<void>}
367
+ */
368
+ async sendIndication(peerAddress: string, peerPort: number, data: Buffer): Promise<void> {
369
+ if (!this.#username || !this.#credential) {
370
+ throw new Error('TURN requires username and credential');
371
+ }
372
+
373
+ const transactionId = crypto.randomBytes(12);
374
+ const indication = this.#createSendIndication(transactionId, peerAddress, peerPort, data);
375
+
376
+ // Indications are fire-and-forget, no response expected
377
+ return new Promise<void>((resolve, reject) => {
378
+ this.#socket!.send(indication, this.#port, this.#server, (err) => {
379
+ if (err) reject(err);
380
+ else resolve();
381
+ });
382
+ });
383
+ }
384
+
385
+ /**
386
+ * Send a TURN request
387
+ * @param {Buffer} request - Request message
388
+ * @param {Buffer} transactionId - Transaction ID
389
+ * @param {string} requestType - Type of request
390
+ * @returns {Promise<Object>}
391
+ * @private
392
+ */
393
+ #sendRequest(request: Buffer, transactionId: Buffer, requestType: string): Promise<TransactionResult> {
394
+ return new Promise<TransactionResult>((resolve, reject) => {
395
+ const timeout = setTimeout(() => {
396
+ this.#transactions.delete(transactionId.toString('hex'));
397
+ reject(new Error(`${requestType} request timeout`));
398
+ }, 5000);
399
+
400
+ this.#transactions.set(transactionId.toString('hex'), {
401
+ type: requestType,
402
+ resolve: (result: TransactionResult) => {
403
+ clearTimeout(timeout);
404
+ resolve(result);
405
+ },
406
+ reject: (error: Error) => {
407
+ clearTimeout(timeout);
408
+ reject(error);
409
+ },
410
+ });
411
+
412
+ this.#socket!.send(request, this.#port, this.#server, (err) => {
413
+ if (err) {
414
+ clearTimeout(timeout);
415
+ this.#transactions.delete(transactionId.toString('hex'));
416
+ reject(err);
417
+ }
418
+ });
419
+ });
420
+ }
421
+
422
+ /**
423
+ * Create a STUN Binding Request
424
+ * @param {Buffer} transactionId - Transaction ID
425
+ * @returns {Buffer} STUN message
426
+ * @private
427
+ */
428
+ #createBindingRequest(transactionId: Buffer): Buffer {
429
+ const header = Buffer.alloc(20);
430
+
431
+ // Message Type (2 bytes)
432
+ header.writeUInt16BE(STUN_MESSAGE_TYPES.BINDING_REQUEST, 0);
433
+
434
+ // Message Length (2 bytes) - 0 for now, no attributes
435
+ header.writeUInt16BE(0, 2);
436
+
437
+ // Magic Cookie (4 bytes)
438
+ header.writeUInt32BE(MAGIC_COOKIE, 4);
439
+
440
+ // Transaction ID (12 bytes)
441
+ transactionId.copy(header, 8);
442
+
443
+ return header;
444
+ }
445
+
446
+ /**
447
+ * Create a TURN Allocate Request
448
+ * @param {Buffer} transactionId - Transaction ID
449
+ * @param {number} lifetime - Allocation lifetime in seconds
450
+ * @param {boolean} withAuth - Include authentication
451
+ * @returns {Buffer} STUN message
452
+ * @private
453
+ */
454
+ #createAllocateRequest(transactionId: Buffer, lifetime: number, withAuth: boolean = false): Buffer {
455
+ const attributes: Buffer[] = [];
456
+
457
+ // REQUESTED-TRANSPORT (UDP = 17)
458
+ const transport = Buffer.alloc(8);
459
+ transport.writeUInt16BE(STUN_ATTRIBUTES.REQUESTED_TRANSPORT, 0);
460
+ transport.writeUInt16BE(4, 2);
461
+ transport.writeUInt8(17, 4); // UDP
462
+ attributes.push(transport);
463
+
464
+ // LIFETIME
465
+ const lifetimeAttr = Buffer.alloc(8);
466
+ lifetimeAttr.writeUInt16BE(STUN_ATTRIBUTES.LIFETIME, 0);
467
+ lifetimeAttr.writeUInt16BE(4, 2);
468
+ lifetimeAttr.writeUInt32BE(lifetime, 4);
469
+ attributes.push(lifetimeAttr);
470
+
471
+ if (withAuth && this.#realm && this.#nonce) {
472
+ // USERNAME
473
+ const usernameAttr = this.#createStringAttribute(STUN_ATTRIBUTES.USERNAME, this.#username!);
474
+ attributes.push(usernameAttr);
475
+
476
+ // REALM
477
+ const realmAttr = this.#createStringAttribute(STUN_ATTRIBUTES.REALM, this.#realm);
478
+ attributes.push(realmAttr);
479
+
480
+ // NONCE
481
+ const nonceAttr = this.#createStringAttribute(STUN_ATTRIBUTES.NONCE, this.#nonce);
482
+ attributes.push(nonceAttr);
483
+ }
484
+
485
+ return this.#createMessage(STUN_MESSAGE_TYPES.ALLOCATE_REQUEST, transactionId, attributes, withAuth);
486
+ }
487
+
488
+ /**
489
+ * Create a TURN CreatePermission Request
490
+ * @param {Buffer} transactionId - Transaction ID
491
+ * @param {string} peerAddress - Peer IP address
492
+ * @returns {Buffer} STUN message
493
+ * @private
494
+ */
495
+ #createCreatePermissionRequest(transactionId: Buffer, peerAddress: string): Buffer {
496
+ const attributes: Buffer[] = [];
497
+
498
+ // XOR-PEER-ADDRESS
499
+ const peerAttr = this.#createXorPeerAddressAttribute(peerAddress, 0, transactionId);
500
+ attributes.push(peerAttr);
501
+
502
+ // Auth attributes
503
+ if (this.#realm && this.#nonce) {
504
+ attributes.push(this.#createStringAttribute(STUN_ATTRIBUTES.USERNAME, this.#username!));
505
+ attributes.push(this.#createStringAttribute(STUN_ATTRIBUTES.REALM, this.#realm));
506
+ attributes.push(this.#createStringAttribute(STUN_ATTRIBUTES.NONCE, this.#nonce));
507
+ }
508
+
509
+ return this.#createMessage(STUN_MESSAGE_TYPES.CREATE_PERMISSION_REQUEST, transactionId, attributes, true);
510
+ }
511
+
512
+ /**
513
+ * Create a TURN Send Indication
514
+ * @param {Buffer} transactionId - Transaction ID
515
+ * @param {string} peerAddress - Peer IP address
516
+ * @param {number} peerPort - Peer port
517
+ * @param {Buffer} data - Data to send
518
+ * @returns {Buffer} STUN message
519
+ * @private
520
+ */
521
+ #createSendIndication(transactionId: Buffer, peerAddress: string, peerPort: number, data: Buffer): Buffer {
522
+ const attributes: Buffer[] = [];
523
+
524
+ // XOR-PEER-ADDRESS
525
+ const peerAttr = this.#createXorPeerAddressAttribute(peerAddress, peerPort, transactionId);
526
+ attributes.push(peerAttr);
527
+
528
+ // DATA
529
+ const dataAttr = Buffer.alloc(4 + data.length + ((4 - (data.length % 4)) % 4));
530
+ dataAttr.writeUInt16BE(STUN_ATTRIBUTES.DATA, 0);
531
+ dataAttr.writeUInt16BE(data.length, 2);
532
+ data.copy(dataAttr, 4);
533
+ attributes.push(dataAttr);
534
+
535
+ return this.#createMessage(STUN_MESSAGE_TYPES.SEND_INDICATION, transactionId, attributes, false);
536
+ }
537
+
538
+ /**
539
+ * Create XOR-PEER-ADDRESS attribute
540
+ * @param {string} address - IP address
541
+ * @param {number} port - Port
542
+ * @param {Buffer} transactionId - Transaction ID
543
+ * @returns {Buffer} Attribute buffer
544
+ * @private
545
+ */
546
+ #createXorPeerAddressAttribute(address: string, port: number, _transactionId: Buffer): Buffer {
547
+ const family = 0x01; // IPv4
548
+ const buffer = Buffer.alloc(4 + 8); // Type(2) + Length(2) + Reserved(1) + Family(1) + Port(2) + Address(4)
549
+
550
+ buffer.writeUInt16BE(STUN_ATTRIBUTES.XOR_PEER_ADDRESS, 0);
551
+ buffer.writeUInt16BE(8, 2);
552
+ buffer.writeUInt8(0, 4);
553
+ buffer.writeUInt8(family, 5);
554
+
555
+ // XOR Port
556
+ const xorPort = port ^ (MAGIC_COOKIE >> 16);
557
+ buffer.writeUInt16BE(xorPort, 6);
558
+
559
+ // XOR Address
560
+ const parts = address.split('.').map(Number);
561
+ const addrInt = (parts[0]! << 24) | (parts[1]! << 16) | (parts[2]! << 8) | parts[3]!;
562
+ const xorAddr = addrInt ^ MAGIC_COOKIE;
563
+
564
+ buffer.writeUInt32BE(xorAddr >>> 0, 8); // Ensure unsigned
565
+
566
+ return buffer;
567
+ }
568
+
569
+ /**
570
+ * Create a TURN Refresh Request
571
+ * @param {Buffer} transactionId - Transaction ID
572
+ * @param {number} lifetime - Allocation lifetime in seconds
573
+ * @returns {Buffer} STUN message
574
+ * @private
575
+ */
576
+ #createRefreshRequest(transactionId: Buffer, lifetime: number): Buffer {
577
+ const attributes: Buffer[] = [];
578
+
579
+ // LIFETIME
580
+ const lifetimeAttr = Buffer.alloc(8);
581
+ lifetimeAttr.writeUInt16BE(STUN_ATTRIBUTES.LIFETIME, 0);
582
+ lifetimeAttr.writeUInt16BE(4, 2);
583
+ lifetimeAttr.writeUInt32BE(lifetime, 4);
584
+ attributes.push(lifetimeAttr);
585
+
586
+ // USERNAME
587
+ const usernameAttr = this.#createStringAttribute(STUN_ATTRIBUTES.USERNAME, this.#username!);
588
+ attributes.push(usernameAttr);
589
+
590
+ // REALM
591
+ if (this.#realm) {
592
+ const realmAttr = this.#createStringAttribute(STUN_ATTRIBUTES.REALM, this.#realm);
593
+ attributes.push(realmAttr);
594
+ }
595
+
596
+ // NONCE
597
+ if (this.#nonce) {
598
+ const nonceAttr = this.#createStringAttribute(STUN_ATTRIBUTES.NONCE, this.#nonce);
599
+ attributes.push(nonceAttr);
600
+ }
601
+
602
+ return this.#createMessage(STUN_MESSAGE_TYPES.REFRESH_REQUEST, transactionId, attributes, true);
603
+ }
604
+
605
+ /**
606
+ * Create a STUN message with attributes
607
+ * @param {number} messageType - Message type
608
+ * @param {Buffer} transactionId - Transaction ID
609
+ * @param {Array<Buffer>} attributes - Attribute buffers
610
+ * @param {boolean} withIntegrity - Add MESSAGE-INTEGRITY
611
+ * @returns {Buffer} Complete STUN message
612
+ * @private
613
+ */
614
+ #createMessage(
615
+ messageType: number,
616
+ transactionId: Buffer,
617
+ attributes: Buffer[],
618
+ withIntegrity: boolean = false
619
+ ): Buffer {
620
+ let attributesBuffer = Buffer.concat(attributes);
621
+
622
+ // Add MESSAGE-INTEGRITY if needed
623
+ if (withIntegrity && this.#credential) {
624
+ const tempHeader = Buffer.alloc(20);
625
+ tempHeader.writeUInt16BE(messageType, 0);
626
+ tempHeader.writeUInt16BE(attributesBuffer.length + 24, 2); // +24 for MESSAGE-INTEGRITY
627
+ tempHeader.writeUInt32BE(MAGIC_COOKIE, 4);
628
+ transactionId.copy(tempHeader, 8);
629
+
630
+ const tempMessage = Buffer.concat([tempHeader, attributesBuffer]);
631
+
632
+ // For TURN, compute key as MD5(username:realm:password) per RFC 5766
633
+ let key: string | Buffer = this.#credential;
634
+ if (this.#username && this.#realm) {
635
+ const keyString = `${this.#username}:${this.#realm}:${this.#credential}`;
636
+ key = crypto.createHash('md5').update(keyString).digest();
637
+ }
638
+
639
+ const hmac = crypto.createHmac('sha1', key);
640
+ hmac.update(tempMessage);
641
+ const integrity = hmac.digest();
642
+
643
+ const integrityAttr = Buffer.alloc(4 + integrity.length);
644
+ integrityAttr.writeUInt16BE(STUN_ATTRIBUTES.MESSAGE_INTEGRITY, 0);
645
+ integrityAttr.writeUInt16BE(integrity.length, 2);
646
+ integrity.copy(integrityAttr, 4);
647
+
648
+ attributesBuffer = Buffer.concat([attributesBuffer, integrityAttr]);
649
+ }
650
+
651
+ // Create final message
652
+ const header = Buffer.alloc(20);
653
+ header.writeUInt16BE(messageType, 0);
654
+ header.writeUInt16BE(attributesBuffer.length, 2);
655
+ header.writeUInt32BE(MAGIC_COOKIE, 4);
656
+ transactionId.copy(header, 8);
657
+
658
+ return Buffer.concat([header, attributesBuffer]);
659
+ }
660
+
661
+ /**
662
+ * Create a string attribute
663
+ * @param {number} type - Attribute type
664
+ * @param {string} value - String value
665
+ * @returns {Buffer} Attribute buffer
666
+ * @private
667
+ */
668
+ #createStringAttribute(type: number, value: string): Buffer {
669
+ const valueBuffer = Buffer.from(value, 'utf8');
670
+ const length = valueBuffer.length;
671
+ const padding = (4 - (length % 4)) % 4;
672
+ const buffer = Buffer.alloc(4 + length + padding);
673
+
674
+ buffer.writeUInt16BE(type, 0);
675
+ buffer.writeUInt16BE(length, 2);
676
+ valueBuffer.copy(buffer, 4);
677
+
678
+ return buffer;
679
+ }
680
+
681
+ /**
682
+ * Handle incoming STUN message
683
+ * @param {Buffer} msg - Message buffer
684
+ * @param {Object} rinfo - Remote info
685
+ * @private
686
+ */
687
+ #handleMessage(msg: Buffer, _rinfo: dgram.RemoteInfo): void {
688
+ if (msg.length < 20) {
689
+ return; // Invalid STUN message
690
+ }
691
+
692
+ const messageType = msg.readUInt16BE(0);
693
+ const messageLength = msg.readUInt16BE(2);
694
+ const magicCookie = msg.readUInt32BE(4);
695
+ const transactionId = msg.slice(8, 20);
696
+
697
+ if (magicCookie !== MAGIC_COOKIE) {
698
+ return; // Not a STUN message
699
+ }
700
+
701
+ // DATA indications are server-initiated (relayed peer data) and carry a
702
+ // fresh transaction id that matches no pending request — handle them before
703
+ // the transaction lookup.
704
+ if (messageType === STUN_MESSAGE_TYPES.DATA_INDICATION) {
705
+ const attrs = this.#parseAttributes(msg.slice(20, 20 + messageLength), transactionId);
706
+ if (attrs.xorPeerAddress && attrs.data) {
707
+ const info: DataEventInfo = {
708
+ address: attrs.xorPeerAddress.address,
709
+ port: attrs.xorPeerAddress.port,
710
+ family: attrs.xorPeerAddress.family || 'IPv4',
711
+ };
712
+ this.emit('data', attrs.data, info);
713
+ }
714
+ return;
715
+ }
716
+
717
+ const transactionKey = transactionId.toString('hex');
718
+ const transaction = this.#transactions.get(transactionKey);
719
+
720
+ if (!transaction) {
721
+ return; // Unknown transaction
722
+ }
723
+
724
+ const attributes = this.#parseAttributes(msg.slice(20, 20 + messageLength), transactionId);
725
+
726
+ // Handle STUN Binding responses
727
+ if (messageType === STUN_MESSAGE_TYPES.BINDING_RESPONSE) {
728
+ if (attributes.xorMappedAddress) {
729
+ transaction.resolve({
730
+ address: attributes.xorMappedAddress.address,
731
+ port: attributes.xorMappedAddress.port,
732
+ family: attributes.xorMappedAddress.family,
733
+ });
734
+ } else if (attributes.mappedAddress) {
735
+ transaction.resolve({
736
+ address: attributes.mappedAddress.address,
737
+ port: attributes.mappedAddress.port,
738
+ family: attributes.mappedAddress.family,
739
+ });
740
+ } else {
741
+ transaction.reject(new Error('No mapped address in STUN response'));
742
+ }
743
+ this.#transactions.delete(transactionKey);
744
+ }
745
+ // Handle TURN Allocate responses
746
+ else if (messageType === STUN_MESSAGE_TYPES.ALLOCATE_RESPONSE) {
747
+ if (attributes.xorRelayedAddress) {
748
+ transaction.resolve({
749
+ relayedAddress: attributes.xorRelayedAddress.address,
750
+ relayedPort: attributes.xorRelayedAddress.port,
751
+ lifetime: attributes.lifetime || 600,
752
+ type: 'relay',
753
+ });
754
+ } else {
755
+ transaction.reject(new Error('No relayed address in ALLOCATE response'));
756
+ }
757
+ this.#transactions.delete(transactionKey);
758
+ }
759
+ // Handle TURN Refresh responses
760
+ else if (messageType === STUN_MESSAGE_TYPES.REFRESH_RESPONSE) {
761
+ transaction.resolve({
762
+ lifetime: attributes.lifetime || 600,
763
+ });
764
+ this.#transactions.delete(transactionKey);
765
+ }
766
+ // Handle TURN CreatePermission / ChannelBind success responses
767
+ else if (
768
+ messageType === STUN_MESSAGE_TYPES.CREATE_PERMISSION_RESPONSE ||
769
+ messageType === STUN_MESSAGE_TYPES.CHANNEL_BIND_RESPONSE
770
+ ) {
771
+ transaction.resolve({ ok: true });
772
+ this.#transactions.delete(transactionKey);
773
+ }
774
+ // (DATA indications are handled earlier, before the transaction lookup.)
775
+ // Handle error responses generically: any class-of-error message
776
+ // (the 0x0110 bits set). This covers ALLOCATE (0x0113), CreatePermission
777
+ // (0x0118), Refresh (0x0114), Binding (0x0111), etc.
778
+ else if ((messageType & 0x0110) === 0x0110) {
779
+ // Store realm and nonce so the caller can retry with fresh credentials.
780
+ if (attributes.realm) {
781
+ this.#realm = attributes.realm;
782
+ }
783
+ if (attributes.nonce) {
784
+ this.#nonce = attributes.nonce;
785
+ }
786
+
787
+ const errorMsg = attributes.errorCode || 'Unknown error';
788
+ transaction.reject(new Error(`STUN error: ${errorMsg}`));
789
+ this.#transactions.delete(transactionKey);
790
+ }
791
+ }
792
+
793
+ /**
794
+ * Parse STUN attributes
795
+ * @param {Buffer} data - Attributes data
796
+ * @param {Buffer} transactionId - Transaction ID
797
+ * @returns {Object} Parsed attributes
798
+ * @private
799
+ */
800
+ #parseAttributes(data: Buffer, transactionId: Buffer): ParsedAttributes {
801
+ const attributes: ParsedAttributes = {};
802
+ let offset = 0;
803
+
804
+ while (offset < data.length) {
805
+ if (offset + 4 > data.length) break;
806
+
807
+ const type = data.readUInt16BE(offset);
808
+ const length = data.readUInt16BE(offset + 2);
809
+ offset += 4;
810
+
811
+ if (offset + length > data.length) break;
812
+
813
+ const value = data.slice(offset, offset + length);
814
+
815
+ switch (type) {
816
+ case STUN_ATTRIBUTES.XOR_MAPPED_ADDRESS:
817
+ attributes.xorMappedAddress = this.#parseXorAddress(value, transactionId);
818
+ break;
819
+ case STUN_ATTRIBUTES.XOR_RELAYED_ADDRESS:
820
+ attributes.xorRelayedAddress = this.#parseXorAddress(value, transactionId);
821
+ break;
822
+ case STUN_ATTRIBUTES.XOR_PEER_ADDRESS:
823
+ attributes.xorPeerAddress = this.#parseXorAddress(value, transactionId);
824
+ break;
825
+ case STUN_ATTRIBUTES.DATA:
826
+ attributes.data = value;
827
+ break;
828
+ case STUN_ATTRIBUTES.MAPPED_ADDRESS:
829
+ attributes.mappedAddress = this.#parseAddress(value);
830
+ break;
831
+ case STUN_ATTRIBUTES.LIFETIME:
832
+ attributes.lifetime = value.readUInt32BE(0);
833
+ break;
834
+ case STUN_ATTRIBUTES.ERROR_CODE:
835
+ attributes.errorCode = this.#parseErrorCode(value);
836
+ break;
837
+ case STUN_ATTRIBUTES.REALM:
838
+ attributes.realm = value.toString('utf8');
839
+ this.#realm = attributes.realm;
840
+ break;
841
+ case STUN_ATTRIBUTES.NONCE:
842
+ attributes.nonce = value.toString('utf8');
843
+ this.#nonce = attributes.nonce;
844
+ break;
845
+ }
846
+
847
+ // Pad to 4-byte boundary
848
+ offset += length;
849
+ const padding = (4 - (length % 4)) % 4;
850
+ offset += padding;
851
+ }
852
+
853
+ return attributes;
854
+ }
855
+
856
+ /**
857
+ * Parse XOR-MAPPED-ADDRESS attribute
858
+ * @param {Buffer} data - Attribute data
859
+ * @param {Buffer} transactionId - Transaction ID
860
+ * @returns {Object} Address info
861
+ * @private
862
+ */
863
+ #parseXorAddress(data: Buffer, _transactionId: Buffer): AddressInfo | null {
864
+ const family = data.readUInt8(1);
865
+ const xorPort = data.readUInt16BE(2);
866
+
867
+ // XOR port with magic cookie high 16 bits
868
+ const port = xorPort ^ (MAGIC_COOKIE >> 16);
869
+
870
+ if (family === 0x01) {
871
+ // IPv4
872
+ const xorAddress = data.readUInt32BE(4);
873
+ const address = xorAddress ^ MAGIC_COOKIE;
874
+
875
+ return {
876
+ family: 'IPv4',
877
+ port,
878
+ address: [(address >> 24) & 0xff, (address >> 16) & 0xff, (address >> 8) & 0xff, address & 0xff].join('.'),
879
+ };
880
+ }
881
+
882
+ return null;
883
+ }
884
+
885
+ /**
886
+ * Parse MAPPED-ADDRESS attribute
887
+ * @param {Buffer} data - Attribute data
888
+ * @returns {Object} Address info
889
+ * @private
890
+ */
891
+ #parseAddress(data: Buffer): AddressInfo | null {
892
+ const family = data.readUInt8(1);
893
+ const port = data.readUInt16BE(2);
894
+
895
+ if (family === 0x01) {
896
+ // IPv4
897
+ const address = data.slice(4, 8);
898
+ return {
899
+ family: 'IPv4',
900
+ port,
901
+ address: Array.from(address).join('.'),
902
+ };
903
+ }
904
+
905
+ return null;
906
+ }
907
+
908
+ /**
909
+ * Parse ERROR-CODE attribute
910
+ * @param {Buffer} data - Attribute data
911
+ * @returns {string} Error message
912
+ * @private
913
+ */
914
+ #parseErrorCode(data: Buffer): string {
915
+ const errorClass = data.readUInt8(2) & 0x07;
916
+ const errorNumber = data.readUInt8(3);
917
+ const errorCode = errorClass * 100 + errorNumber;
918
+ const reason = data.slice(4).toString('utf8');
919
+
920
+ return `${errorCode} ${reason}`;
921
+ }
922
+
923
+ /**
924
+ * Close the client
925
+ */
926
+ close(): void {
927
+ if (this.#socket) {
928
+ this.#socket.close();
929
+ this.#socket = null;
930
+ }
931
+ this.#transactions.clear();
932
+ }
933
+ }
934
+
935
+ export default STUNClient;
936
+ export { STUNClient };