node-rtc-connection 1.0.3

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.
@@ -0,0 +1,315 @@
1
+ /**
2
+ * ICE Candidate Gatherer
3
+ * Discovers local, reflexive (STUN), and relay (TURN) candidates
4
+ */
5
+
6
+ const os = require('os');
7
+ const dgram = require('dgram');
8
+ const STUNClient = require('./STUNClient');
9
+ const TURNClient = require('./TURNClient');
10
+
11
+ class ICEGatherer {
12
+ constructor(options = {}) {
13
+ this.stunServers = options.stunServers || [
14
+ 'stun.l.google.com:19302',
15
+ 'stun1.l.google.com:19302',
16
+ 'stun2.l.google.com:19302'
17
+ ];
18
+ this.turnServers = options.turnServers || [];
19
+ this.gatherTimeout = options.gatherTimeout || 5000;
20
+ }
21
+
22
+ /**
23
+ * Gather all ICE candidates
24
+ * @param {number} localPort - Local port being used
25
+ * @returns {Promise<Array>} Array of ICE candidates
26
+ */
27
+ async gatherCandidates(localPort) {
28
+ const candidates = [];
29
+
30
+ // 1. Gather host candidates (local interfaces)
31
+ const hostCandidates = this._getHostCandidates(localPort);
32
+ candidates.push(...hostCandidates);
33
+
34
+ // 2. Gather server reflexive candidates (via STUN)
35
+ try {
36
+ const srflxCandidates = await this._getServerReflexiveCandidates(localPort);
37
+ candidates.push(...srflxCandidates);
38
+ } catch (err) {
39
+ console.warn('Failed to gather STUN candidates:', err.message);
40
+ }
41
+
42
+ // 3. Gather relay candidates (via TURN)
43
+ try {
44
+ const relayCandidates = await this._getRelayCandidates(localPort);
45
+ candidates.push(...relayCandidates);
46
+ } catch (err) {
47
+ console.warn('Failed to gather TURN candidates:', err.message);
48
+ }
49
+
50
+ // 4. Sort by priority (host > srflx > relay)
51
+ candidates.sort((a, b) => b.priority - a.priority);
52
+
53
+ return candidates;
54
+ }
55
+
56
+ /**
57
+ * Get host candidates from local network interfaces
58
+ * @private
59
+ */
60
+ _getHostCandidates(port) {
61
+ const candidates = [];
62
+ const interfaces = os.networkInterfaces();
63
+ let foundation = 1;
64
+
65
+ for (const [name, addrs] of Object.entries(interfaces)) {
66
+ for (const addr of addrs) {
67
+ // Skip internal and IPv6 for now
68
+ if (addr.internal || addr.family !== 'IPv4') {
69
+ continue;
70
+ }
71
+
72
+ const priority = this._calculatePriority('host', 65535, foundation);
73
+
74
+ candidates.push({
75
+ candidate: `candidate:${foundation} 1 udp ${priority} ${addr.address} ${port} typ host`,
76
+ sdpMLineIndex: 0,
77
+ sdpMid: 'data',
78
+ foundation: String(foundation),
79
+ component: 1,
80
+ protocol: 'udp',
81
+ priority,
82
+ ip: addr.address,
83
+ port,
84
+ type: 'host',
85
+ tcpType: null,
86
+ relatedAddress: null,
87
+ relatedPort: null
88
+ });
89
+
90
+ foundation++;
91
+ }
92
+ }
93
+
94
+ return candidates;
95
+ }
96
+
97
+ /**
98
+ * Get server reflexive candidates via STUN
99
+ * @private
100
+ */
101
+ async _getServerReflexiveCandidates(localPort) {
102
+ const candidates = [];
103
+ const stunPromises = [];
104
+
105
+ // Try multiple STUN servers in parallel
106
+ for (const stunServer of this.stunServers) {
107
+ const promise = this._querySTUNServer(stunServer, localPort)
108
+ .catch(err => null); // Ignore individual failures
109
+ stunPromises.push(promise);
110
+ }
111
+
112
+ // Wait for first successful response
113
+ const results = await Promise.race([
114
+ Promise.any(stunPromises.filter(p => p)),
115
+ new Promise(resolve => setTimeout(() => resolve(null), this.gatherTimeout))
116
+ ]);
117
+
118
+ if (results) {
119
+ const foundation = 100;
120
+ const priority = this._calculatePriority('srflx', 65535, foundation);
121
+
122
+ candidates.push({
123
+ candidate: `candidate:${foundation} 1 udp ${priority} ${results.ip} ${results.port} typ srflx raddr ${results.localIp} rport ${localPort}`,
124
+ sdpMLineIndex: 0,
125
+ sdpMid: 'data',
126
+ foundation: String(foundation),
127
+ component: 1,
128
+ protocol: 'udp',
129
+ priority,
130
+ ip: results.ip,
131
+ port: results.port,
132
+ type: 'srflx',
133
+ tcpType: null,
134
+ relatedAddress: results.localIp,
135
+ relatedPort: localPort
136
+ });
137
+ }
138
+
139
+ return candidates;
140
+ }
141
+
142
+ /**
143
+ * Query a STUN server
144
+ * @private
145
+ */
146
+ async _querySTUNServer(stunServer, localPort) {
147
+ const client = new STUNClient();
148
+ try {
149
+ const result = await client.getReflexiveAddress(stunServer);
150
+
151
+ // Get local IP that would be used to reach STUN server
152
+ const localIp = this._getLocalIPForRemote();
153
+
154
+ return {
155
+ ...result,
156
+ localIp
157
+ };
158
+ } finally {
159
+ client.close();
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Get local IP address that would be used for external connections
165
+ * @private
166
+ */
167
+ _getLocalIPForRemote() {
168
+ const interfaces = os.networkInterfaces();
169
+
170
+ // Prefer non-internal IPv4 addresses
171
+ for (const [name, addrs] of Object.entries(interfaces)) {
172
+ for (const addr of addrs) {
173
+ if (!addr.internal && addr.family === 'IPv4') {
174
+ return addr.address;
175
+ }
176
+ }
177
+ }
178
+
179
+ return '0.0.0.0';
180
+ }
181
+
182
+ /**
183
+ * Get relay candidates via TURN
184
+ * @private
185
+ */
186
+ async _getRelayCandidates(localPort) {
187
+ const candidates = [];
188
+
189
+ if (this.turnServers.length === 0) {
190
+ return candidates;
191
+ }
192
+
193
+ const turnPromises = [];
194
+
195
+ // Try TURN servers
196
+ for (const turnConfig of this.turnServers) {
197
+ const promise = this._queryTURNServer(turnConfig, localPort)
198
+ .catch(err => null); // Ignore individual failures
199
+ turnPromises.push(promise);
200
+ }
201
+
202
+ // Wait for first successful response
203
+ const results = await Promise.race([
204
+ Promise.any(turnPromises.filter(p => p)),
205
+ new Promise(resolve => setTimeout(() => resolve(null), this.gatherTimeout))
206
+ ]);
207
+
208
+ if (results) {
209
+ const foundation = 200;
210
+ const priority = this._calculatePriority('relay', 65535, foundation);
211
+
212
+ const localIp = this._getLocalIPForRemote();
213
+
214
+ candidates.push({
215
+ candidate: `candidate:${foundation} 1 udp ${priority} ${results.relayedAddress} ${results.relayedPort} typ relay raddr ${localIp} rport ${localPort}`,
216
+ sdpMLineIndex: 0,
217
+ sdpMid: 'data',
218
+ foundation: String(foundation),
219
+ component: 1,
220
+ protocol: 'udp',
221
+ priority,
222
+ ip: results.relayedAddress,
223
+ port: results.relayedPort,
224
+ type: 'relay',
225
+ tcpType: null,
226
+ relatedAddress: localIp,
227
+ relatedPort: localPort
228
+ });
229
+ }
230
+
231
+ return candidates;
232
+ }
233
+
234
+ /**
235
+ * Query a TURN server
236
+ * @private
237
+ */
238
+ async _queryTURNServer(turnConfig, localPort) {
239
+ const client = new TURNClient({
240
+ server: turnConfig.urls || turnConfig.url,
241
+ username: turnConfig.username,
242
+ password: turnConfig.credential,
243
+ transport: 'udp'
244
+ });
245
+
246
+ try {
247
+ const result = await client.allocate();
248
+ return result;
249
+ } finally {
250
+ client.close();
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Calculate ICE candidate priority (RFC 5245)
256
+ * @private
257
+ */
258
+ _calculatePriority(type, localPref, foundation) {
259
+ const typePreference = {
260
+ 'host': 126,
261
+ 'srflx': 100,
262
+ 'prflx': 110,
263
+ 'relay': 0
264
+ };
265
+
266
+ const typePref = typePreference[type] || 0;
267
+ const componentId = 1; // RTP component
268
+
269
+ // Priority = (2^24)*(type preference) + (2^8)*(local preference) + (256 - component ID)
270
+ return (typePref << 24) + (localPref << 8) + (256 - componentId);
271
+ }
272
+
273
+ /**
274
+ * Parse ICE candidate string
275
+ * @param {string} candidateStr - ICE candidate string
276
+ * @returns {Object} Parsed candidate object
277
+ */
278
+ static parseCandidate(candidateStr) {
279
+ // Remove "candidate:" prefix if present
280
+ const str = candidateStr.replace(/^candidate:/, '');
281
+ const parts = str.split(' ');
282
+
283
+ if (parts.length < 8) {
284
+ throw new Error('Invalid candidate string');
285
+ }
286
+
287
+ const candidate = {
288
+ foundation: parts[0],
289
+ component: parseInt(parts[1], 10),
290
+ protocol: parts[2].toLowerCase(),
291
+ priority: parseInt(parts[3], 10),
292
+ ip: parts[4],
293
+ port: parseInt(parts[5], 10),
294
+ type: parts[7]
295
+ };
296
+
297
+ // Parse optional fields (raddr, rport, etc.)
298
+ for (let i = 8; i < parts.length; i += 2) {
299
+ const key = parts[i];
300
+ const value = parts[i + 1];
301
+
302
+ if (key === 'raddr') {
303
+ candidate.relatedAddress = value;
304
+ } else if (key === 'rport') {
305
+ candidate.relatedPort = parseInt(value, 10);
306
+ } else if (key === 'tcptype') {
307
+ candidate.tcpType = value;
308
+ }
309
+ }
310
+
311
+ return candidate;
312
+ }
313
+ }
314
+
315
+ module.exports = ICEGatherer;