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
@@ -1,998 +0,0 @@
1
- /**
2
- * @file RTCIceTransport.js
3
- * @description ICE transport implementation for establishing connectivity.
4
- * @module ice/RTCIceTransport
5
- *
6
- * Ported from Chromium's RTCIceTransport implementation:
7
- * - cc/rtc_ice_transport.h
8
- * - cc/rtc_ice_transport.cc
9
- * - cc/rtc_ice_transport.idl
10
- */
11
-
12
- const EventEmitter = require('events');
13
- const STUNClient = require('../stun/stun-client');
14
- const dgram = require('dgram');
15
- const crypto = require('crypto');
16
-
17
- /**
18
- * RTCIceRole - The role in the ICE process
19
- * @readonly
20
- * @enum {string}
21
- */
22
- const RTCIceRole = Object.freeze({
23
- CONTROLLING: 'controlling',
24
- CONTROLLED: 'controlled'
25
- });
26
-
27
- /**
28
- * RTCIceTransportState - Current state of the ICE transport
29
- * @readonly
30
- * @enum {string}
31
- */
32
- const RTCIceTransportState = Object.freeze({
33
- NEW: 'new',
34
- CHECKING: 'checking',
35
- CONNECTED: 'connected',
36
- COMPLETED: 'completed',
37
- DISCONNECTED: 'disconnected',
38
- FAILED: 'failed',
39
- CLOSED: 'closed'
40
- });
41
-
42
- /**
43
- * RTCIceGatheringState - ICE candidate gathering state
44
- * @readonly
45
- * @enum {string}
46
- */
47
- const RTCIceGatheringState = Object.freeze({
48
- NEW: 'new',
49
- GATHERING: 'gathering',
50
- COMPLETE: 'complete'
51
- });
52
-
53
- /**
54
- * RTCIceParameters - ICE username fragment and password
55
- * @typedef {Object} RTCIceParameters
56
- * @property {string} usernameFragment - ICE username fragment
57
- * @property {string} password - ICE password
58
- */
59
-
60
- /**
61
- * RTCIceCandidatePair - A pair of local and remote ICE candidates
62
- * @typedef {Object} RTCIceCandidatePair
63
- * @property {RTCIceCandidate} local - The local candidate
64
- * @property {RTCIceCandidate} remote - The remote candidate
65
- */
66
-
67
- /**
68
- * RTCIceGatherOptions - Options for ICE candidate gathering
69
- * @typedef {Object} RTCIceGatherOptions
70
- * @property {RTCIceGatherPolicy} [gatherPolicy='all'] - Candidate gathering policy
71
- * @property {Array<RTCIceServer>} [iceServers] - STUN/TURN servers to use
72
- */
73
-
74
- /**
75
- * @class RTCIceTransport
76
- * @extends EventEmitter
77
- * @description Represents the ICE transport layer for a peer connection.
78
- * Manages ICE candidate gathering, connectivity checks, and state transitions.
79
- *
80
- * Events:
81
- * - 'statechange': Fired when the transport state changes
82
- * - 'gatheringstatechange': Fired when the gathering state changes
83
- * - 'selectedcandidatepairchange': Fired when the selected candidate pair changes
84
- * - 'icecandidate': Fired when a new local candidate is gathered
85
- *
86
- * @example
87
- * const transport = new RTCIceTransport();
88
- * transport.on('statechange', () => {
89
- * console.log('State:', transport.state);
90
- * });
91
- * transport.on('icecandidate', (candidate) => {
92
- * // Send candidate to remote peer
93
- * });
94
- */
95
- class RTCIceTransport extends EventEmitter {
96
- /**
97
- * Create an RTCIceTransport instance.
98
- */
99
- constructor() {
100
- super();
101
-
102
- // Internal state
103
- this._role = null; // RTCIceRole or null
104
- this._state = RTCIceTransportState.NEW;
105
- this._gatheringState = RTCIceGatheringState.NEW;
106
-
107
- // Candidate lists
108
- this._localCandidates = [];
109
- this._remoteCandidates = [];
110
-
111
- // ICE parameters
112
- this._localParameters = null;
113
- this._remoteParameters = null;
114
-
115
- // Selected candidate pair
116
- this._selectedCandidatePair = null;
117
-
118
- // Started flag
119
- this._started = false;
120
-
121
- // Closed flag
122
- this._closed = false;
123
-
124
- // ICE servers (STUN/TURN)
125
- this._iceServers = [];
126
-
127
- // STUN clients
128
- this._stunClients = [];
129
-
130
- // UDP sockets for candidates
131
- this._sockets = new Map(); // Map of candidate foundation -> socket
132
-
133
- // Candidate pairs and connectivity checks
134
- this._candidatePairs = [];
135
- this._validPairs = [];
136
- }
137
-
138
- /**
139
- * Get the ICE role (controlling or controlled).
140
- * @returns {string|null} The ICE role or null if not started
141
- */
142
- get role() {
143
- return this._role;
144
- }
145
-
146
- /**
147
- * Get the current ICE transport state.
148
- * @returns {string} The transport state
149
- */
150
- get state() {
151
- return this._state;
152
- }
153
-
154
- /**
155
- * Get the current ICE gathering state.
156
- * @returns {string} The gathering state
157
- */
158
- get gatheringState() {
159
- return this._gatheringState;
160
- }
161
-
162
- /**
163
- * Get the local ICE candidates.
164
- * @returns {Array<RTCIceCandidate>} Array of local candidates
165
- */
166
- getLocalCandidates() {
167
- return [...this._localCandidates];
168
- }
169
-
170
- /**
171
- * Get the remote ICE candidates.
172
- * @returns {Array<RTCIceCandidate>} Array of remote candidates
173
- */
174
- getRemoteCandidates() {
175
- return [...this._remoteCandidates];
176
- }
177
-
178
- /**
179
- * Get the selected candidate pair.
180
- * @returns {RTCIceCandidatePair|null} The selected candidate pair or null
181
- */
182
- getSelectedCandidatePair() {
183
- return this._selectedCandidatePair ? { ...this._selectedCandidatePair } : null;
184
- }
185
-
186
- /**
187
- * Get the local ICE parameters.
188
- * @returns {RTCIceParameters|null} The local parameters or null
189
- */
190
- getLocalParameters() {
191
- return this._localParameters ? { ...this._localParameters } : null;
192
- }
193
-
194
- /**
195
- * Get the remote ICE parameters.
196
- * @returns {RTCIceParameters|null} The remote parameters or null
197
- */
198
- getRemoteParameters() {
199
- return this._remoteParameters ? { ...this._remoteParameters } : null;
200
- }
201
-
202
- /**
203
- * Generate random local ICE parameters.
204
- * Creates a random username fragment and password.
205
- * @private
206
- */
207
- _generateLocalParameters() {
208
- const crypto = require('crypto');
209
-
210
- // Generate random username fragment (16 characters)
211
- const usernameFragment = crypto.randomBytes(8).toString('hex');
212
-
213
- // Generate random password (22 characters, base64)
214
- const password = crypto.randomBytes(16).toString('base64').substring(0, 22);
215
-
216
- this._localParameters = {
217
- usernameFragment,
218
- password
219
- };
220
- }
221
-
222
- /**
223
- * Start the ICE transport with remote parameters and role.
224
- * @param {RTCIceParameters} remoteParameters - The remote ICE parameters
225
- * @param {string} role - The ICE role ('controlling' or 'controlled')
226
- * @throws {Error} If transport is closed or already started
227
- * @throws {TypeError} If parameters are invalid
228
- */
229
- start(remoteParameters, role) {
230
- if (this._closed) {
231
- throw new Error('RTCIceTransport is closed');
232
- }
233
-
234
- if (this._started) {
235
- throw new Error('RTCIceTransport already started');
236
- }
237
-
238
- // Validate role
239
- if (role !== RTCIceRole.CONTROLLING && role !== RTCIceRole.CONTROLLED) {
240
- throw new TypeError(`Invalid role: ${role}`);
241
- }
242
-
243
- // Validate remote parameters
244
- if (!remoteParameters || typeof remoteParameters !== 'object') {
245
- throw new TypeError('Remote parameters must be an object');
246
- }
247
-
248
- if (typeof remoteParameters.usernameFragment !== 'string' ||
249
- remoteParameters.usernameFragment.length === 0) {
250
- throw new TypeError('usernameFragment must be a non-empty string');
251
- }
252
-
253
- if (typeof remoteParameters.password !== 'string' ||
254
- remoteParameters.password.length === 0) {
255
- throw new TypeError('password must be a non-empty string');
256
- }
257
-
258
- // Generate local parameters if not already done
259
- if (!this._localParameters) {
260
- this._generateLocalParameters();
261
- }
262
-
263
- // Store remote parameters and role
264
- this._remoteParameters = {
265
- usernameFragment: remoteParameters.usernameFragment,
266
- password: remoteParameters.password
267
- };
268
- this._role = role;
269
- this._started = true;
270
-
271
- // Transition to checking state
272
- this._setState(RTCIceTransportState.CHECKING);
273
-
274
- // Start connectivity checks if we have candidates
275
- if (this._remoteCandidates.length > 0 && this._localCandidates.length > 0) {
276
- this._startConnectivityChecks();
277
- }
278
- }
279
-
280
- /**
281
- * Start gathering ICE candidates.
282
- * @param {RTCIceGatherOptions} [options] - Gathering options
283
- * @throws {Error} If transport is closed
284
- */
285
- async gather(options = {}) {
286
- if (this._closed) {
287
- throw new Error('RTCIceTransport is closed');
288
- }
289
-
290
- // Generate local parameters if not already done
291
- if (!this._localParameters) {
292
- this._generateLocalParameters();
293
- }
294
-
295
- // Store ICE servers
296
- if (options.iceServers) {
297
- this._iceServers = options.iceServers;
298
- }
299
-
300
- // Transition to gathering state
301
- this._setGatheringState(RTCIceGatheringState.GATHERING);
302
-
303
- // Gather host candidates (local addresses)
304
- await this._gatherHostCandidates();
305
-
306
- // Gather server reflexive candidates (STUN)
307
- await this._gatherServerReflexiveCandidates();
308
-
309
- // Gather relay candidates (TURN)
310
- await this._gatherRelayCandidates();
311
-
312
- // Complete gathering
313
- setImmediate(() => {
314
- if (!this._closed) {
315
- this._setGatheringState(RTCIceGatheringState.COMPLETE);
316
- }
317
- });
318
- }
319
-
320
- /**
321
- * Add a remote ICE candidate.
322
- * @param {RTCIceCandidate} candidate - The remote candidate to add
323
- * @throws {Error} If transport is closed or not started
324
- * @throws {TypeError} If candidate is invalid
325
- */
326
- addRemoteCandidate(candidate) {
327
- if (this._closed) {
328
- throw new Error('RTCIceTransport is closed');
329
- }
330
-
331
- if (!this._started) {
332
- throw new Error('RTCIceTransport not started');
333
- }
334
-
335
- if (!candidate || typeof candidate !== 'object') {
336
- throw new TypeError('Candidate must be an object');
337
- }
338
-
339
- // Ensure candidate is properly formatted
340
- // If it's a plain object with a candidate string, parse it
341
- let parsedCandidate = candidate;
342
- const RTCIceCandidate = require('./RTCIceCandidate');
343
-
344
- if (!(candidate instanceof RTCIceCandidate)) {
345
- // Try to create an RTCIceCandidate to parse and validate
346
- try {
347
- parsedCandidate = new RTCIceCandidate(candidate);
348
- } catch (err) {
349
- console.warn('Failed to parse remote candidate:', err.message);
350
- // Store the original anyway for compatibility
351
- this._remoteCandidates.push(candidate);
352
- return;
353
- }
354
- } else {
355
- parsedCandidate = candidate;
356
- }
357
-
358
- // Validate that candidate has required properties for connectivity checks
359
- if (!parsedCandidate.address || parsedCandidate.port === null || parsedCandidate.port === undefined) {
360
- console.warn('Remote candidate missing address or port, skipping connectivity checks');
361
- // Store the original candidate for compatibility
362
- this._remoteCandidates.push(candidate);
363
- return;
364
- }
365
-
366
- // Store the original candidate (for getRemoteCandidates compatibility)
367
- this._remoteCandidates.push(candidate);
368
-
369
- // Start connectivity checks if we're in checking state
370
- if (this._state === RTCIceTransportState.CHECKING && this._localCandidates.length > 0) {
371
- // Create pairs using the parsed candidate for connectivity checks
372
- for (const localCandidate of this._localCandidates) {
373
- const pair = {
374
- local: localCandidate,
375
- remote: parsedCandidate,
376
- state: 'waiting'
377
- };
378
- this._candidatePairs.push(pair);
379
- this._sendConnectivityCheck(pair);
380
- }
381
- }
382
- }
383
-
384
- /**
385
- * Stop the ICE transport.
386
- * Transitions to closed state and stops all ICE processing.
387
- */
388
- stop() {
389
- if (this._closed) {
390
- return;
391
- }
392
-
393
- this._close('stopped');
394
- }
395
-
396
- /**
397
- * Internal close method.
398
- * @param {string} reason - Reason for closing
399
- * @private
400
- */
401
- _close(reason) {
402
- if (this._closed) {
403
- return;
404
- }
405
-
406
- this._closed = true;
407
- this._setState(RTCIceTransportState.CLOSED);
408
-
409
- // Close all UDP sockets
410
- this._closeSockets();
411
-
412
- // Clean up STUN/TURN clients
413
- if (this._stunClients) {
414
- for (const client of this._stunClients) {
415
- client.close();
416
- }
417
- this._stunClients = [];
418
- }
419
-
420
- // Clear refresh timers
421
- if (this._refreshTimers) {
422
- for (const timer of this._refreshTimers) {
423
- clearInterval(timer);
424
- }
425
- this._refreshTimers = [];
426
- }
427
-
428
- // Clean up
429
- this._localCandidates = [];
430
- this._remoteCandidates = [];
431
- this._selectedCandidatePair = null;
432
- }
433
-
434
- /**
435
- * Set the transport state and emit event if changed.
436
- * @param {string} newState - The new state
437
- * @private
438
- */
439
- _setState(newState) {
440
- if (this._state !== newState) {
441
- this._state = newState;
442
- this.emit('statechange');
443
- }
444
- }
445
-
446
- /**
447
- * Set the gathering state and emit event if changed.
448
- * @param {string} newState - The new gathering state
449
- * @private
450
- */
451
- _setGatheringState(newState) {
452
- if (this._gatheringState !== newState) {
453
- this._gatheringState = newState;
454
- this.emit('gatheringstatechange');
455
- }
456
- }
457
-
458
- /**
459
- * Add a local candidate (called internally during gathering).
460
- * @param {RTCIceCandidate} candidate - The local candidate
461
- * @private
462
- */
463
- _addLocalCandidate(candidate) {
464
- this._localCandidates.push(candidate);
465
- this.emit('icecandidate', candidate);
466
- }
467
-
468
- /**
469
- * Set the selected candidate pair.
470
- * @param {RTCIceCandidatePair} pair - The selected pair
471
- * @private
472
- */
473
- _setSelectedCandidatePair(pair) {
474
- this._selectedCandidatePair = pair;
475
- this.emit('selectedcandidatepairchange');
476
- }
477
-
478
- /**
479
- * Check if the transport is in a terminal state.
480
- * @returns {boolean} True if closed, false otherwise
481
- */
482
- isClosed() {
483
- return this._state === RTCIceTransportState.CLOSED;
484
- }
485
-
486
- /**
487
- * Gather host candidates (local network interfaces)
488
- * @private
489
- */
490
- async _gatherHostCandidates() {
491
- const os = require('os');
492
- const interfaces = os.networkInterfaces();
493
-
494
- for (const [name, addrs] of Object.entries(interfaces)) {
495
- for (const addr of addrs) {
496
- // Skip internal and IPv6 for now
497
- if (addr.internal || addr.family !== 'IPv4') {
498
- continue;
499
- }
500
-
501
- // Create UDP socket for this candidate
502
- const socket = dgram.createSocket('udp4');
503
-
504
- await new Promise((resolve, reject) => {
505
- socket.on('error', reject);
506
-
507
- // Bind to the interface address with port 0 (auto-assign)
508
- socket.bind(0, addr.address, () => {
509
- const address = socket.address();
510
-
511
- const RTCIceCandidate = require('./RTCIceCandidate');
512
- const foundation = crypto.randomBytes(4).toString('hex');
513
-
514
- // Generate SDP candidate string
515
- const candidateString = `candidate:${foundation} 1 udp ${2130706431} ${address.address} ${address.port} typ host`;
516
-
517
- const candidate = new RTCIceCandidate({
518
- candidate: candidateString,
519
- sdpMid: '0',
520
- sdpMLineIndex: 0,
521
- foundation,
522
- priority: 2130706431, // Host candidates have highest priority
523
- address: address.address,
524
- protocol: 'udp',
525
- port: address.port,
526
- type: 'host',
527
- component: 'rtp',
528
- usernameFragment: this._localParameters.usernameFragment
529
- });
530
-
531
- // Store socket associated with this candidate
532
- this._sockets.set(foundation, socket);
533
-
534
- // Setup socket message handler for ICE connectivity checks
535
- socket.on('message', (msg, rinfo) => {
536
- this._handleSocketMessage(msg, rinfo, candidate);
537
- });
538
-
539
- socket.on('error', (err) => {
540
- console.error(`Socket error for ${address.address}:${address.port}:`, err);
541
- });
542
-
543
- this._addLocalCandidate(candidate);
544
- resolve();
545
- });
546
- });
547
- }
548
- }
549
- }
550
-
551
- /**
552
- * Gather server reflexive candidates using STUN
553
- * @private
554
- */
555
- async _gatherServerReflexiveCandidates() {
556
- if (!this._iceServers || this._iceServers.length === 0) {
557
- return;
558
- }
559
-
560
- // Use the first host candidate's socket for STUN queries
561
- const hostCandidates = this._localCandidates.filter(c => c.type === 'host');
562
- if (hostCandidates.length === 0) {
563
- return; // No host candidates to use
564
- }
565
-
566
- for (const server of this._iceServers) {
567
- const urls = Array.isArray(server.urls) ? server.urls : [server.urls];
568
-
569
- for (const url of urls) {
570
- if (!url.startsWith('stun:')) {
571
- continue; // Skip non-STUN servers
572
- }
573
-
574
- try {
575
- const parsed = this._parseServerUrl(url);
576
- const stunClient = new STUNClient({
577
- server: parsed.host,
578
- port: parsed.port,
579
- username: server.username,
580
- credential: server.credential,
581
- transport: parsed.transport,
582
- params: parsed.params
583
- });
584
-
585
- this._stunClients.push(stunClient);
586
-
587
- const reflexiveAddr = await stunClient.getReflexiveAddress();
588
-
589
- const RTCIceCandidate = require('./RTCIceCandidate');
590
- const foundation = crypto.randomBytes(4).toString('hex');
591
- const hostCandidate = hostCandidates[0];
592
-
593
- // Generate SDP candidate string for server reflexive candidate
594
- const candidateString = `candidate:${foundation} 1 udp ${1694498815} ${reflexiveAddr.address} ${reflexiveAddr.port} typ srflx raddr ${hostCandidate.address} rport ${hostCandidate.port}`;
595
-
596
- const candidate = new RTCIceCandidate({
597
- candidate: candidateString,
598
- sdpMid: '0',
599
- sdpMLineIndex: 0,
600
- foundation,
601
- priority: 1694498815, // Server reflexive candidates
602
- address: reflexiveAddr.address,
603
- protocol: 'udp',
604
- port: reflexiveAddr.port,
605
- type: 'srflx',
606
- component: 'rtp',
607
- relatedAddress: hostCandidate.address,
608
- relatedPort: hostCandidate.port,
609
- usernameFragment: this._localParameters.usernameFragment
610
- });
611
-
612
- // Store the socket (same as host candidate's socket)
613
- this._sockets.set(foundation, this._sockets.get(hostCandidate.foundation));
614
-
615
- this._addLocalCandidate(candidate);
616
- } catch (error) {
617
- console.error(`Failed to gather from STUN server ${url}:`, error.message);
618
- }
619
- }
620
- }
621
- }
622
-
623
- /**
624
- * Gather relay candidates using TURN
625
- * @private
626
- */
627
- async _gatherRelayCandidates() {
628
- if (!this._iceServers || this._iceServers.length === 0) {
629
- return;
630
- }
631
-
632
- for (const server of this._iceServers) {
633
- const urls = Array.isArray(server.urls) ? server.urls : [server.urls];
634
-
635
- for (const url of urls) {
636
- if (!url.startsWith('turn:') && !url.startsWith('turns:')) {
637
- continue; // Skip non-TURN servers
638
- }
639
-
640
- // TURN requires authentication
641
- if (!server.username || !server.credential) {
642
- console.warn(`TURN server ${url} requires username and credential`);
643
- continue;
644
- }
645
-
646
- try {
647
- const parsed = this._parseServerUrl(url);
648
- const turnClient = new STUNClient({
649
- server: parsed.host,
650
- port: parsed.port,
651
- username: server.username,
652
- credential: server.credential,
653
- transport: parsed.transport,
654
- params: parsed.params
655
- });
656
-
657
- this._stunClients.push(turnClient);
658
-
659
- // Allocate relay address
660
- const allocation = await turnClient.allocateRelay(600);
661
-
662
- const RTCIceCandidate = require('./RTCIceCandidate');
663
- const foundation = crypto.randomBytes(4).toString('hex');
664
-
665
- // Generate SDP candidate string for relay candidate
666
- const candidateString = `candidate:${foundation} 1 udp ${16777215} ${allocation.relayedAddress} ${allocation.relayedPort} typ relay raddr ${parsed.host} rport ${parsed.port}`;
667
-
668
- const candidate = new RTCIceCandidate({
669
- candidate: candidateString,
670
- sdpMid: '0',
671
- sdpMLineIndex: 0,
672
- foundation,
673
- priority: 16777215, // Relay candidates have lowest priority
674
- address: allocation.relayedAddress,
675
- protocol: 'udp',
676
- port: allocation.relayedPort,
677
- type: 'relay',
678
- component: 'rtp',
679
- relatedAddress: parsed.host, // TURN server address
680
- relatedPort: parsed.port,
681
- usernameFragment: this._localParameters.usernameFragment
682
- });
683
-
684
- // Store TURN client as the "socket" for this relay candidate
685
- this._sockets.set(foundation, { type: 'turn', client: turnClient });
686
-
687
- this._addLocalCandidate(candidate);
688
-
689
- // Keep allocation alive
690
- this._keepAllocAlive(turnClient, allocation.lifetime);
691
- } catch (error) {
692
- console.error(`Failed to allocate from TURN server ${url}:`, error.message);
693
- }
694
- }
695
- }
696
- }
697
-
698
- /**
699
- * Keep TURN allocation alive by sending periodic refresh requests
700
- * @param {STUNClient} client - TURN client
701
- * @param {number} lifetime - Allocation lifetime
702
- * @private
703
- */
704
- _keepAllocAlive(client, lifetime) {
705
- // Refresh 30 seconds before expiry
706
- const refreshInterval = (lifetime - 30) * 1000;
707
-
708
- const refreshTimer = setInterval(async () => {
709
- if (this._closed) {
710
- clearInterval(refreshTimer);
711
- return;
712
- }
713
-
714
- try {
715
- await client.refreshAllocation(600);
716
- } catch (error) {
717
- console.error('Failed to refresh TURN allocation:', error.message);
718
- clearInterval(refreshTimer);
719
- }
720
- }, refreshInterval);
721
-
722
- // Store timer for cleanup
723
- if (!this._refreshTimers) {
724
- this._refreshTimers = [];
725
- }
726
- this._refreshTimers.push(refreshTimer);
727
- }
728
-
729
- /**
730
- * Start ICE connectivity checks
731
- * @private
732
- */
733
- _startConnectivityChecks() {
734
- // Form candidate pairs (simplified: just pair first local with first remote)
735
- for (const localCandidate of this._localCandidates) {
736
- for (const remoteCandidate of this._remoteCandidates) {
737
- this._candidatePairs.push({
738
- local: localCandidate,
739
- remote: remoteCandidate,
740
- state: 'waiting'
741
- });
742
- }
743
- }
744
-
745
- // Send connectivity checks for each pair
746
- for (const pair of this._candidatePairs) {
747
- this._sendConnectivityCheck(pair);
748
- }
749
- }
750
-
751
- /**
752
- * Send a connectivity check (STUN Binding Request) to a candidate pair
753
- * @param {Object} pair - Candidate pair
754
- * @private
755
- */
756
- /**
757
- * Send ICE connectivity check to remote candidate
758
- * @param {Object} pair - Candidate pair
759
- * @private
760
- */
761
- _sendConnectivityCheck(pair) {
762
- const socket = this._sockets.get(pair.local.foundation);
763
- if (!socket || socket.type === 'turn') {
764
- return; // Skip TURN candidates for now
765
- }
766
-
767
- // Validate remote candidate has required properties
768
- if (!pair.remote.address || !pair.remote.port) {
769
- console.warn('Cannot send connectivity check: remote candidate missing address or port', {
770
- address: pair.remote.address,
771
- port: pair.remote.port,
772
- candidate: pair.remote.candidate
773
- });
774
- return;
775
- }
776
-
777
- const transactionId = crypto.randomBytes(12);
778
- const request = this._createBindingRequest(transactionId);
779
-
780
- try {
781
- socket.send(request, pair.remote.port, pair.remote.address, (err) => {
782
- if (err) {
783
- console.error(`Connectivity check failed for ${pair.remote.address}:${pair.remote.port}:`, err);
784
- }
785
- });
786
- } catch (err) {
787
- console.error(`Error sending connectivity check to ${pair.remote.address || 'unknown'}:${pair.remote.port || 'unknown'}:`, err);
788
- }
789
- }
790
-
791
- /**
792
- * Create a STUN Binding Request for connectivity checks
793
- * @param {Buffer} transactionId - Transaction ID
794
- * @returns {Buffer} STUN Binding Request
795
- * @private
796
- */
797
- _createBindingRequest(transactionId) {
798
- const MAGIC_COOKIE = 0x2112A442;
799
-
800
- // Simple binding request with no attributes
801
- const header = Buffer.alloc(20);
802
- header.writeUInt16BE(0x0001, 0); // Binding Request
803
- header.writeUInt16BE(0, 2); // Message length (no attributes for now)
804
- header.writeUInt32BE(MAGIC_COOKIE, 4);
805
- transactionId.copy(header, 8);
806
-
807
- return header;
808
- }
809
-
810
- /**
811
- * Parse STUN/TURN server URL
812
- * @param {string} url - Server URL
813
- * @returns {Object} Parsed URL
814
- * @private
815
- */
816
- /**
817
- * Parse server URL with query string support
818
- * @param {string} url - Server URL (e.g., turn:host:port?transport=udp&ttl=86400)
819
- * @returns {Object} Parsed server info
820
- * @private
821
- */
822
- _parseServerUrl(url) {
823
- // Match: (stun|turn|turns)://host:port?query or (stun|turn|turns):host:port?query
824
- const match = url.match(/^(stun|turn|turns):\/?\/?([^:?]+):?(\d+)?(\?(.+))?/);
825
- if (!match) {
826
- throw new Error(`Invalid server URL: ${url}`);
827
- }
828
-
829
- const protocol = match[1];
830
- const host = match[2];
831
- const port = match[3];
832
- const queryString = match[5];
833
-
834
- // Parse query parameters
835
- const params = {};
836
- if (queryString) {
837
- queryString.split('&').forEach(param => {
838
- const [key, value] = param.split('=');
839
- if (key) {
840
- // If value is undefined (no = sign), set to true
841
- // If value is empty string, keep it as empty string
842
- params[key] = value !== undefined ? value : true;
843
- }
844
- });
845
- }
846
-
847
- return {
848
- protocol,
849
- host,
850
- port: parseInt(port || (protocol === 'turns' ? '5349' : '3478'), 10),
851
- transport: params.transport || 'udp', // Default to UDP
852
- params // Include all query parameters for future use
853
- };
854
- }
855
-
856
- /**
857
- * Handle incoming messages on a socket (ICE connectivity checks)
858
- * @param {Buffer} msg - The message buffer
859
- * @param {Object} rinfo - Remote address info
860
- * @param {RTCIceCandidate} localCandidate - The local candidate that received the message
861
- * @private
862
- */
863
- _handleSocketMessage(msg, rinfo, localCandidate) {
864
- // Check if this is a STUN message (magic cookie check)
865
- if (msg.length < 20) return;
866
-
867
- const magicCookie = msg.readUInt32BE(4);
868
- if (magicCookie !== 0x2112A442) return; // Not a STUN message
869
-
870
- const messageType = msg.readUInt16BE(0);
871
-
872
- // STUN Binding Request (0x0001) - this is an ICE connectivity check
873
- if (messageType === 0x0001) {
874
- this._handleBindingRequest(msg, rinfo, localCandidate);
875
- }
876
- // STUN Binding Response (0x0101) - response to our connectivity check
877
- else if (messageType === 0x0101) {
878
- this._handleBindingResponse(msg, rinfo, localCandidate);
879
- }
880
- }
881
-
882
- /**
883
- * Handle STUN Binding Request (incoming connectivity check)
884
- * @param {Buffer} msg - The STUN message
885
- * @param {Object} rinfo - Remote address info
886
- * @param {RTCIceCandidate} localCandidate - The local candidate
887
- * @private
888
- */
889
- _handleBindingRequest(msg, rinfo, localCandidate) {
890
- // Send Binding Response
891
- const transactionId = msg.slice(8, 20);
892
- const response = this._createBindingResponse(transactionId, rinfo.address, rinfo.port);
893
-
894
- const socket = this._sockets.get(localCandidate.foundation);
895
- if (socket) {
896
- socket.send(response, rinfo.port, rinfo.address);
897
-
898
- // If we haven't selected a candidate pair yet and this check succeeded,
899
- // this could be our selected pair
900
- if (!this._selectedCandidatePair && this._state === RTCIceTransportState.CHECKING) {
901
- this._setState(RTCIceTransportState.CONNECTED);
902
- }
903
- }
904
- }
905
-
906
- /**
907
- * Handle STUN Binding Response (response to our connectivity check)
908
- * @param {Buffer} msg - The STUN message
909
- * @param {Object} rinfo - Remote address info
910
- * @param {RTCIceCandidate} localCandidate - The local candidate
911
- * @private
912
- */
913
- _handleBindingResponse(msg, rinfo, localCandidate) {
914
- // Mark this candidate pair as valid
915
- // In a full implementation, we would track which pair this belongs to
916
- if (!this._selectedCandidatePair && this._state === RTCIceTransportState.CHECKING) {
917
- this._setState(RTCIceTransportState.CONNECTED);
918
- }
919
- }
920
-
921
- /**
922
- * Create a STUN Binding Response
923
- * @param {Buffer} transactionId - Transaction ID from the request
924
- * @param {string} address - XOR-mapped address
925
- * @param {number} port - XOR-mapped port
926
- * @returns {Buffer} STUN Binding Response message
927
- * @private
928
- */
929
- _createBindingResponse(transactionId, address, port) {
930
- const MAGIC_COOKIE = 0x2112A442;
931
-
932
- // XOR the port with magic cookie high 16 bits
933
- const xorPort = port ^ (MAGIC_COOKIE >> 16);
934
-
935
- // XOR the address with magic cookie
936
- const addrParts = address.split('.').map(Number);
937
- const addrNum = (addrParts[0] << 24) | (addrParts[1] << 16) | (addrParts[2] << 8) | addrParts[3];
938
- const xorAddr = addrNum ^ MAGIC_COOKIE;
939
-
940
- // Build XOR-MAPPED-ADDRESS attribute
941
- const attr = Buffer.alloc(12);
942
- attr.writeUInt16BE(0x0020, 0); // XOR-MAPPED-ADDRESS
943
- attr.writeUInt16BE(8, 2); // Length
944
- attr.writeUInt8(0, 4); // Reserved
945
- attr.writeUInt8(0x01, 5); // Family (IPv4)
946
- attr.writeUInt16BE(xorPort, 6);
947
- attr.writeUInt32BE(xorAddr, 8);
948
-
949
- // Build message
950
- const header = Buffer.alloc(20);
951
- header.writeUInt16BE(0x0101, 0); // Binding Response
952
- header.writeUInt16BE(12, 2); // Message length (attribute size)
953
- header.writeUInt32BE(MAGIC_COOKIE, 4);
954
- transactionId.copy(header, 8);
955
-
956
- return Buffer.concat([header, attr]);
957
- }
958
-
959
- /**
960
- * Check if start() has been called.
961
- * @returns {boolean} True if started, false otherwise
962
- */
963
- isStarted() {
964
- return this._started;
965
- }
966
-
967
- /**
968
- * Get a socket for data transmission (returns the first available socket)
969
- * @returns {Object|null} UDP socket or null
970
- */
971
- getSocket() {
972
- const sockets = Array.from(this._sockets.values());
973
- return sockets.length > 0 ? sockets[0] : null;
974
- }
975
-
976
- /**
977
- * Close all sockets
978
- * @private
979
- */
980
- _closeSockets() {
981
- for (const socket of this._sockets.values()) {
982
- try {
983
- socket.close();
984
- } catch (err) {
985
- // Ignore errors when closing
986
- }
987
- }
988
- this._sockets.clear();
989
- }
990
- }
991
-
992
- // Export the class and enums
993
- module.exports = {
994
- RTCIceTransport,
995
- RTCIceRole,
996
- RTCIceTransportState,
997
- RTCIceGatheringState
998
- };