node-rtc-connection 2.0.4 → 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 (53) hide show
  1. package/index.cjs +1 -0
  2. package/index.mjs +1 -0
  3. package/package.json +10 -47
  4. package/dist/index.cjs +0 -32
  5. package/dist/index.mjs +0 -43
  6. package/src/crypto/der.ts +0 -205
  7. package/src/crypto/x509.ts +0 -146
  8. package/src/datachannel/RTCDataChannel.ts +0 -388
  9. package/src/dtls/RTCCertificate.ts +0 -396
  10. package/src/dtls/cipher.ts +0 -198
  11. package/src/dtls/connection.ts +0 -974
  12. package/src/dtls/prf.ts +0 -62
  13. package/src/dtls/protocol.ts +0 -204
  14. package/src/foundation/ByteBufferQueue.ts +0 -237
  15. package/src/foundation/RTCError.ts +0 -276
  16. package/src/ice/RTCIceCandidate.ts +0 -349
  17. package/src/ice/ice-agent.ts +0 -609
  18. package/src/ice/stun-message.ts +0 -260
  19. package/src/index.ts +0 -72
  20. package/src/peerconnection/RTCPeerConnection.ts +0 -430
  21. package/src/sctp/association.ts +0 -523
  22. package/src/sctp/chunks.ts +0 -350
  23. package/src/sctp/crc32c.ts +0 -57
  24. package/src/sctp/datachannel-manager.ts +0 -187
  25. package/src/sctp/dcep.ts +0 -94
  26. package/src/sdp/RTCSessionDescription.ts +0 -115
  27. package/src/sdp/sdp-utils.ts +0 -229
  28. package/src/stun/stun-client.ts +0 -936
  29. package/src/transport-stack.ts +0 -165
  30. /package/{dist/types → types}/crypto/der.d.ts +0 -0
  31. /package/{dist/types → types}/crypto/x509.d.ts +0 -0
  32. /package/{dist/types → types}/datachannel/RTCDataChannel.d.ts +0 -0
  33. /package/{dist/types → types}/dtls/RTCCertificate.d.ts +0 -0
  34. /package/{dist/types → types}/dtls/cipher.d.ts +0 -0
  35. /package/{dist/types → types}/dtls/connection.d.ts +0 -0
  36. /package/{dist/types → types}/dtls/prf.d.ts +0 -0
  37. /package/{dist/types → types}/dtls/protocol.d.ts +0 -0
  38. /package/{dist/types → types}/foundation/ByteBufferQueue.d.ts +0 -0
  39. /package/{dist/types → types}/foundation/RTCError.d.ts +0 -0
  40. /package/{dist/types → types}/ice/RTCIceCandidate.d.ts +0 -0
  41. /package/{dist/types → types}/ice/ice-agent.d.ts +0 -0
  42. /package/{dist/types → types}/ice/stun-message.d.ts +0 -0
  43. /package/{dist/types → types}/index.d.ts +0 -0
  44. /package/{dist/types → types}/peerconnection/RTCPeerConnection.d.ts +0 -0
  45. /package/{dist/types → types}/sctp/association.d.ts +0 -0
  46. /package/{dist/types → types}/sctp/chunks.d.ts +0 -0
  47. /package/{dist/types → types}/sctp/crc32c.d.ts +0 -0
  48. /package/{dist/types → types}/sctp/datachannel-manager.d.ts +0 -0
  49. /package/{dist/types → types}/sctp/dcep.d.ts +0 -0
  50. /package/{dist/types → types}/sdp/RTCSessionDescription.d.ts +0 -0
  51. /package/{dist/types → types}/sdp/sdp-utils.d.ts +0 -0
  52. /package/{dist/types → types}/stun/stun-client.d.ts +0 -0
  53. /package/{dist/types → types}/transport-stack.d.ts +0 -0
@@ -1,609 +0,0 @@
1
- /**
2
- * @file ice-agent.ts
3
- * @description A small but RFC 8445-compliant ICE agent for a single data
4
- * component, with browser-compatible connectivity checks and TURN relay.
5
- * @module ice/ice-agent
6
- *
7
- * Responsibilities:
8
- * - Gather UDP host candidates, server-reflexive (srflx) candidates via STUN,
9
- * and relay candidates via TURN (RFC 5766 ALLOCATE).
10
- * - Send/answer STUN Binding connectivity checks carrying USERNAME,
11
- * MESSAGE-INTEGRITY (keyed by the remote/local ice-pwd), PRIORITY, the
12
- * ICE-CONTROLLING/CONTROLLED role attribute, and USE-CANDIDATE.
13
- * - Nominate a candidate pair and expose it as the selected path.
14
- * - Demultiplex inbound datagrams per RFC 7983: STUN (first byte 0-3) is
15
- * handled internally; everything else (DTLS records, first byte 20-63) is
16
- * emitted as 'data' for the upper stack.
17
- *
18
- * Each local candidate carries a `transport` with a uniform interface so the
19
- * connectivity-check and data paths are identical whether the candidate is a
20
- * host socket or a TURN relay:
21
- * transport.send(buf, remoteAddress, remotePort)
22
- * transport.onMessage = (buf, {address, port}) => ...
23
- */
24
-
25
- 'use strict';
26
-
27
- import * as dgram from 'dgram';
28
- import * as os from 'os';
29
- import * as crypto from 'crypto';
30
- import { EventEmitter } from 'events';
31
- import * as S from './stun-message';
32
- import STUNClient from '../stun/stun-client';
33
-
34
- const TYPE_PREF: Record<string, number> = { host: 126, srflx: 100, prflx: 110, relay: 0 };
35
- const CHECK_INTERVAL_MS = 50;
36
- const CHECK_TIMEOUT_MS = 10000;
37
-
38
- /** Remote info accompanying an inbound datagram. */
39
- interface RemoteInfo {
40
- address: string;
41
- port: number;
42
- }
43
-
44
- /**
45
- * Uniform transport abstraction shared by host sockets and TURN relays.
46
- */
47
- interface Transport {
48
- kind: string;
49
- send(buf: Buffer, address: string, port: number): void;
50
- onMessage: ((msg: Buffer, rinfo: RemoteInfo) => void) | null;
51
- close(): void;
52
- }
53
-
54
- /** A local ICE candidate. */
55
- interface LocalCandidate {
56
- foundation: string;
57
- component: number;
58
- protocol: string;
59
- priority: number;
60
- address: string;
61
- port: number;
62
- type: string;
63
- transport: Transport;
64
- sdp: string;
65
- }
66
-
67
- /** A remote ICE candidate (parsed from an a=candidate line or object). */
68
- interface RemoteCandidate {
69
- address: string;
70
- port: number;
71
- priority?: number;
72
- type?: string;
73
- }
74
-
75
- /** A candidate pair under connectivity checking. */
76
- interface CandidatePair {
77
- key?: string;
78
- local: { transport: Transport } & Partial<LocalCandidate>;
79
- remote: { address: string; port: number } & Partial<RemoteCandidate>;
80
- state?: string;
81
- nominated?: boolean;
82
- }
83
-
84
- /** Description of a single ICE server entry. */
85
- interface IceServer {
86
- urls: string | string[];
87
- username?: string;
88
- credential?: string;
89
- }
90
-
91
- /** Options accepted by {@link IceAgent#gather}. */
92
- interface GatherOptions {
93
- iceServers?: IceServer[];
94
- iceTransportPolicy?: 'all' | 'relay';
95
- }
96
-
97
- /** Parsed query parameters from a STUN/TURN URL. */
98
- type IceServerParams = Record<string, string | true>;
99
-
100
- /** Result of {@link parseIceServerUrl}. */
101
- interface ParsedIceServerUrl {
102
- scheme: string;
103
- protocol: string;
104
- host: string;
105
- port: number;
106
- transport: string;
107
- params: IceServerParams;
108
- }
109
-
110
- /** Options accepted by the {@link IceAgent} constructor. */
111
- interface IceAgentOptions {
112
- role: 'controlling' | 'controlled';
113
- localUfrag: string;
114
- localPwd: string;
115
- }
116
-
117
- /** Bookkeeping for a bound host socket and its derived candidate. */
118
- interface HostEntry {
119
- socket: dgram.Socket;
120
- address: string;
121
- port: number;
122
- transport: Transport;
123
- candidate: LocalCandidate;
124
- }
125
-
126
- /** Extra fields stored alongside a candidate (related address/port). */
127
- interface CandidateExtra {
128
- relatedAddress?: string;
129
- relatedPort?: number;
130
- }
131
-
132
- /**
133
- * Compute an ICE candidate priority (RFC 8445 §5.1.2.1).
134
- */
135
- function candidatePriority(type: string, localPref = 65535, componentId = 1): number {
136
- return ((TYPE_PREF[type]! << 24) + (localPref << 8) + (256 - componentId)) >>> 0;
137
- }
138
-
139
- /**
140
- * Parse a STUN/TURN server URL: (stun|turn|turns):host[:port][?key=val&...].
141
- * Query parameters are returned in `params`; a flag without a value (e.g.
142
- * "?secure") is recorded as `true`, an empty value ("?transport=") as "".
143
- * @param {string} url
144
- * @returns {{scheme:string, protocol:string, host:string, port:number,
145
- * transport:string, params:Object}|null} null if the URL is invalid.
146
- */
147
- function parseIceServerUrl(url: string): ParsedIceServerUrl | null {
148
- const m = url.match(/^(stuns?|turns?):\/?\/?([^:?]+):?(\d+)?(?:\?(.+))?$/);
149
- if (!m) return null;
150
- const scheme = m[1]!;
151
- const host = m[2]!;
152
- const params: IceServerParams = {};
153
- if (m[4]) {
154
- for (const kv of m[4].split('&')) {
155
- if (!kv) continue;
156
- const eq = kv.indexOf('=');
157
- if (eq === -1) params[kv] = true; // flag without a value
158
- else params[kv.slice(0, eq)] = kv.slice(eq + 1); // value may be ""
159
- }
160
- }
161
- const defaultPort = scheme === 'turns' || scheme === 'stuns' ? 5349 : 3478;
162
- return {
163
- scheme,
164
- protocol: scheme, // alias
165
- host,
166
- port: parseInt(m[3] || String(defaultPort), 10),
167
- transport: typeof params.transport === 'string' ? params.transport : 'udp',
168
- params,
169
- };
170
- }
171
-
172
- /**
173
- * A host transport: a bound UDP socket. send() targets an arbitrary peer.
174
- */
175
- class HostTransport implements Transport {
176
- kind: string;
177
- #socket: dgram.Socket;
178
- onMessage: ((msg: Buffer, rinfo: RemoteInfo) => void) | null;
179
-
180
- constructor(socket: dgram.Socket) {
181
- this.kind = 'host';
182
- this.#socket = socket;
183
- this.onMessage = null;
184
- socket.on('message', (msg: Buffer, rinfo: dgram.RemoteInfo) => {
185
- if (this.onMessage) this.onMessage(msg, { address: rinfo.address, port: rinfo.port });
186
- });
187
- }
188
- send(buf: Buffer, address: string, port: number): void {
189
- this.#socket.send(buf, port, address);
190
- }
191
- close(): void {
192
- try { this.#socket.close(); } catch (_) {}
193
- }
194
- }
195
-
196
- /**
197
- * A relay transport backed by a TURN allocation. send() installs a permission
198
- * for the peer (idempotent best-effort) and forwards via SEND indication;
199
- * inbound arrives as DATA indications on the TURN client's 'data' event.
200
- */
201
- class RelayTransport implements Transport {
202
- kind: string;
203
- #client: STUNClient;
204
- onMessage: ((msg: Buffer, rinfo: RemoteInfo) => void) | null;
205
- #permitted: Set<string>;
206
-
207
- constructor(turnClient: STUNClient) {
208
- this.kind = 'relay';
209
- this.#client = turnClient;
210
- this.onMessage = null;
211
- this.#permitted = new Set();
212
- turnClient.on('data', (data: Buffer, peer: { address: string; port: number }) => {
213
- if (this.onMessage) this.onMessage(data, { address: peer.address, port: peer.port });
214
- });
215
- }
216
- send(buf: Buffer, address: string, port: number): void {
217
- const key = `${address}:${port}`;
218
- if (!this.#permitted.has(key)) {
219
- this.#permitted.add(key);
220
- // Install permission, then send. Subsequent sends skip the permission.
221
- this.#client.createPermission(address)
222
- .then(() => this.#client.sendIndication(address, port, buf))
223
- .catch(() => {});
224
- } else {
225
- this.#client.sendIndication(address, port, buf).catch(() => {});
226
- }
227
- }
228
- close(): void {
229
- try { this.#client.close(); } catch (_) {}
230
- }
231
- }
232
-
233
- class IceAgent extends EventEmitter {
234
- role: 'controlling' | 'controlled';
235
- localUfrag: string;
236
- localPwd: string;
237
- remoteUfrag: string | null;
238
- remotePwd: string | null;
239
-
240
- #tieBreaker: Buffer;
241
- #transports: Transport[];
242
- #localCandidates: LocalCandidate[];
243
- #remoteCandidates: RemoteCandidate[];
244
- #pairs: CandidatePair[];
245
- #selected: CandidatePair | null;
246
- #closed: boolean;
247
- #checkTimer: NodeJS.Timeout | null;
248
- #timeoutTimer: NodeJS.Timeout | null;
249
- #connected: boolean;
250
- #pendingChecks: Map<string, CandidatePair>;
251
-
252
- /**
253
- * @param {Object} opts
254
- * @param {'controlling'|'controlled'} opts.role
255
- * @param {string} opts.localUfrag
256
- * @param {string} opts.localPwd
257
- */
258
- constructor(opts: IceAgentOptions) {
259
- super();
260
- this.role = opts.role;
261
- this.localUfrag = opts.localUfrag;
262
- this.localPwd = opts.localPwd;
263
- this.remoteUfrag = null;
264
- this.remotePwd = null;
265
-
266
- this.#tieBreaker = crypto.randomBytes(8);
267
- this.#transports = []; // HostTransport | RelayTransport
268
- this.#localCandidates = [];
269
- this.#remoteCandidates = [];
270
- this.#pairs = [];
271
- this.#selected = null;
272
- this.#closed = false;
273
- this.#checkTimer = null;
274
- this.#timeoutTimer = null;
275
- this.#connected = false;
276
- this.#pendingChecks = new Map(); // txid hex -> pair
277
- }
278
-
279
- /**
280
- * Gather candidates. Host candidates always; srflx/relay when iceServers are
281
- * given. With iceTransportPolicy 'relay', only relay candidates are kept.
282
- * @param {Object} [opts]
283
- * @param {Array<{urls:string|string[],username?:string,credential?:string}>} [opts.iceServers]
284
- * @param {'all'|'relay'} [opts.iceTransportPolicy='all']
285
- */
286
- async gather(opts: GatherOptions = {}): Promise<void> {
287
- const iceServers = opts.iceServers || [];
288
- const relayOnly = opts.iceTransportPolicy === 'relay';
289
-
290
- const hostEntries = await this.#gatherHosts();
291
-
292
- // Server-reflexive + relay candidates need a host socket to originate from.
293
- for (const server of iceServers) {
294
- const urls = Array.isArray(server.urls) ? server.urls : [server.urls];
295
- for (const url of urls) {
296
- const parsed = parseIceServerUrl(url);
297
- if (!parsed || parsed.transport !== 'udp') continue; // UDP only for now
298
- try {
299
- if (parsed.scheme === 'stun' && !relayOnly) {
300
- await this.#gatherSrflx(parsed, hostEntries[0]);
301
- } else if (parsed.scheme === 'turn' || parsed.scheme === 'turns') {
302
- await this.#gatherRelay(parsed, server);
303
- }
304
- } catch (err) {
305
- // A failed server must not abort gathering; just skip it.
306
- this.emit('gathererror', { url, error: err instanceof Error ? err.message : String(err) });
307
- }
308
- }
309
- }
310
-
311
- if (relayOnly) {
312
- // Drop host/srflx candidates and their transports from the working set.
313
- this.#localCandidates = this.#localCandidates.filter((c) => c.type === 'relay');
314
- }
315
-
316
- this.emit('gatheringcomplete');
317
- }
318
-
319
- /** Bind one UDP socket per non-internal IPv4 interface; emit host candidates. */
320
- async #gatherHosts(): Promise<HostEntry[]> {
321
- const ifaces = os.networkInterfaces();
322
- const addrs: string[] = [];
323
- for (const list of Object.values(ifaces)) {
324
- if (!list) continue;
325
- for (const a of list) {
326
- if (a.family === 'IPv4' && !a.internal) addrs.push(a.address);
327
- }
328
- }
329
- if (addrs.length === 0) addrs.push('127.0.0.1');
330
-
331
- const entries: HostEntry[] = [];
332
- for (const address of addrs) {
333
- entries.push(await this.#bindHost(address));
334
- }
335
- return entries;
336
- }
337
-
338
- #bindHost(address: string): Promise<HostEntry> {
339
- return new Promise((resolve, _reject) => {
340
- const socket = dgram.createSocket('udp4');
341
- socket.on('error', (err: Error) => this.emit('error', err));
342
- socket.bind(0, address, () => {
343
- const { port } = socket.address();
344
- const transport = new HostTransport(socket);
345
- transport.onMessage = (msg, rinfo) => this.#onDatagram(transport, msg, rinfo);
346
- this.#transports.push(transport);
347
- const cand = this.#addLocalCandidate('host', address, port, transport);
348
- resolve({ socket, address, port, transport, candidate: cand });
349
- });
350
- });
351
- }
352
-
353
- /** Discover the server-reflexive address via a STUN binding request. */
354
- async #gatherSrflx(parsed: ParsedIceServerUrl, hostEntry: HostEntry | undefined): Promise<void> {
355
- if (!hostEntry) return;
356
- const stun = new STUNClient({ server: parsed.host, port: parsed.port });
357
- try {
358
- const addr = await stun.getReflexiveAddress() as { address: string; port: number };
359
- // srflx is reached through the host socket; reuse its transport.
360
- this.#addLocalCandidate('srflx', addr.address, addr.port, hostEntry.transport, {
361
- relatedAddress: hostEntry.address, relatedPort: hostEntry.port,
362
- });
363
- } finally {
364
- stun.close();
365
- }
366
- }
367
-
368
- /** Allocate a TURN relay and expose it as a relay candidate + transport. */
369
- async #gatherRelay(parsed: ParsedIceServerUrl, server: IceServer): Promise<void> {
370
- if (!server.username || !server.credential) {
371
- throw new Error('TURN server requires username and credential');
372
- }
373
- const turn = new STUNClient({
374
- server: parsed.host,
375
- port: parsed.port,
376
- username: server.username,
377
- credential: server.credential,
378
- transport: parsed.transport,
379
- });
380
- const alloc = await turn.allocateRelay(600) as { relayedAddress: string; relayedPort: number };
381
- const transport = new RelayTransport(turn);
382
- transport.onMessage = (msg, rinfo) => this.#onDatagram(transport, msg, rinfo);
383
- this.#transports.push(transport);
384
- this.#addLocalCandidate('relay', alloc.relayedAddress, alloc.relayedPort, transport, {
385
- relatedAddress: parsed.host, relatedPort: parsed.port,
386
- });
387
- }
388
-
389
- #addLocalCandidate(
390
- type: string,
391
- address: string,
392
- port: number,
393
- transport: Transport,
394
- extra: CandidateExtra = {}
395
- ): LocalCandidate {
396
- const foundation = crypto.createHash('md5')
397
- .update(`${type}:${address}:${transport.kind}`).digest('hex').slice(0, 8);
398
- const priority = candidatePriority(type);
399
- let sdp = `candidate:${foundation} 1 udp ${priority} ${address} ${port} typ ${type}`;
400
- if (extra.relatedAddress) {
401
- sdp += ` raddr ${extra.relatedAddress} rport ${extra.relatedPort}`;
402
- }
403
- const cand: LocalCandidate = { foundation, component: 1, protocol: 'udp', priority, address, port, type, transport, sdp };
404
- this.#localCandidates.push(cand);
405
- this.emit('candidate', cand);
406
- return cand;
407
- }
408
-
409
- getLocalCandidates(): LocalCandidate[] {
410
- return this.#localCandidates.slice();
411
- }
412
-
413
- /** Set remote ICE credentials (from the peer's SDP). */
414
- setRemoteCredentials(ufrag: string, pwd: string): void {
415
- this.remoteUfrag = ufrag;
416
- this.remotePwd = pwd;
417
- }
418
-
419
- /**
420
- * Add a remote candidate (parsed from an a=candidate line or object).
421
- * @param {{address:string, port:number, priority?:number, type?:string}} cand
422
- */
423
- addRemoteCandidate(cand: RemoteCandidate): void {
424
- if (!cand || !cand.address || !cand.port) return;
425
- // Browsers obfuscate host candidates as mDNS ".local" hostnames. We don't
426
- // run an mDNS resolver, so these are unusable and sending checks to them
427
- // triggers failing DNS lookups. Skip them — connectivity still succeeds via
428
- // the peer-reflexive candidate we learn from the browser's inbound checks.
429
- if (typeof cand.address === 'string' && cand.address.endsWith('.local')) return;
430
- this.#remoteCandidates.push(cand);
431
- this.#formPairs();
432
- if (!this.#checkTimer && this.remotePwd) this.#startChecks();
433
- }
434
-
435
- /** Begin connectivity checks (call once remote creds + candidates exist). */
436
- start(): void {
437
- if (this.remotePwd && this.#remoteCandidates.length > 0) {
438
- this.#startChecks();
439
- }
440
- }
441
-
442
- #formPairs(): void {
443
- for (const local of this.#localCandidates) {
444
- for (const remote of this.#remoteCandidates) {
445
- const key = `${local.type}:${local.address}:${local.port}->${remote.address}:${remote.port}`;
446
- if (this.#pairs.find((p) => p.key === key)) continue;
447
- this.#pairs.push({ key, local, remote, state: 'frozen', nominated: false });
448
- }
449
- }
450
- }
451
-
452
- #startChecks(): void {
453
- if (this.#checkTimer || this.#closed) return;
454
- this.#checkTimer = setInterval(() => this.#tick(), CHECK_INTERVAL_MS);
455
- if (this.#checkTimer.unref) this.#checkTimer.unref();
456
- this.#timeoutTimer = setTimeout(() => {
457
- if (!this.#connected) this.emit('failed');
458
- this.#stopChecks();
459
- }, CHECK_TIMEOUT_MS);
460
- if (this.#timeoutTimer.unref) this.#timeoutTimer.unref();
461
- this.#tick();
462
- }
463
-
464
- #stopChecks(): void {
465
- if (this.#checkTimer) { clearInterval(this.#checkTimer); this.#checkTimer = null; }
466
- if (this.#timeoutTimer) { clearTimeout(this.#timeoutTimer); this.#timeoutTimer = null; }
467
- }
468
-
469
- #tick(): void {
470
- if (this.#closed) return;
471
- for (const pair of this.#pairs) {
472
- if (pair.state === 'succeeded') continue;
473
- this.#sendCheck(pair);
474
- }
475
- }
476
-
477
- #sendCheck(pair: CandidatePair): void {
478
- const txid = crypto.randomBytes(12);
479
- const username = `${this.remoteUfrag}:${this.localUfrag}`;
480
- const builder = new S.StunMessageBuilder(S.MSG_TYPE.BINDING_REQUEST, txid)
481
- .addUsername(username)
482
- .addPriority(pair.local.priority!);
483
-
484
- if (this.role === 'controlling') {
485
- builder.addIceControlling(this.#tieBreaker);
486
- builder.addUseCandidate(); // aggressive nomination
487
- } else {
488
- builder.addIceControlled(this.#tieBreaker);
489
- }
490
-
491
- const msg = builder.build(this.remotePwd ?? undefined);
492
- this.#pendingChecks.set(txid.toString('hex'), pair);
493
- pair.state = 'in-progress';
494
- pair.local.transport.send(msg, pair.remote.address, pair.remote.port);
495
- }
496
-
497
- #onDatagram(transport: Transport, msg: Buffer, rinfo: RemoteInfo): void {
498
- if (msg.length === 0) return;
499
- const b0 = msg[0]!;
500
- // RFC 7983 demux: 0-3 => STUN, 20-63 => DTLS, else ignore.
501
- if (b0 <= 3) {
502
- this.#onStun(transport, msg, rinfo);
503
- } else {
504
- this.emit('data', msg, { transport, address: rinfo.address, port: rinfo.port });
505
- }
506
- }
507
-
508
- #onStun(transport: Transport, msg: Buffer, rinfo: RemoteInfo): void {
509
- const parsed = S.parse(msg);
510
- if (!parsed) return;
511
- if (parsed.type === S.MSG_TYPE.BINDING_REQUEST) {
512
- this.#handleBindingRequest(transport, parsed, rinfo);
513
- } else if (parsed.type === S.MSG_TYPE.BINDING_SUCCESS) {
514
- this.#handleBindingSuccess(transport, parsed, rinfo);
515
- }
516
- }
517
-
518
- #handleBindingRequest(transport: Transport, parsed: S.ParsedStunMessage, rinfo: RemoteInfo): void {
519
- // Verify MESSAGE-INTEGRITY with our local password (peer keyed it with our pwd).
520
- if (this.localPwd && !S.verifyIntegrity(parsed.raw, this.localPwd)) {
521
- return; // drop unauthenticated checks
522
- }
523
-
524
- const resp = new S.StunMessageBuilder(S.MSG_TYPE.BINDING_SUCCESS, parsed.transactionId)
525
- .addXorMappedAddress(rinfo.address, rinfo.port)
526
- .build(this.localPwd);
527
- transport.send(resp, rinfo.address, rinfo.port);
528
-
529
- // Learn a peer-reflexive remote candidate if unknown.
530
- const known = this.#remoteCandidates.find((c) => c.address === rinfo.address && c.port === rinfo.port);
531
- if (!known) {
532
- this.addRemoteCandidate({ address: rinfo.address, port: rinfo.port, type: 'prflx', priority: 0 });
533
- }
534
-
535
- const useCandidate = parsed.attrs.has(S.ATTR.USE_CANDIDATE);
536
- const pair = this.#findPair(transport, rinfo);
537
-
538
- if (useCandidate && this.role === 'controlled') {
539
- this.#select(pair || this.#syntheticPair(transport, rinfo));
540
- }
541
- }
542
-
543
- #handleBindingSuccess(_transport: Transport, parsed: S.ParsedStunMessage, _rinfo: RemoteInfo): void {
544
- const pair = this.#pendingChecks.get(parsed.transactionId.toString('hex'));
545
- if (!pair) return;
546
- this.#pendingChecks.delete(parsed.transactionId.toString('hex'));
547
- pair.state = 'succeeded';
548
-
549
- if (this.role === 'controlling') {
550
- this.#select(pair);
551
- }
552
- // Controlled agent: a successful check confirms the pair is valid, but the
553
- // path is selected when the controlling peer sends USE-CANDIDATE.
554
- }
555
-
556
- #findPair(transport: Transport, rinfo: RemoteInfo): CandidatePair | undefined {
557
- return this.#pairs.find((p) =>
558
- p.remote.address === rinfo.address && p.remote.port === rinfo.port && p.local.transport === transport);
559
- }
560
-
561
- #syntheticPair(transport: Transport, rinfo: RemoteInfo): CandidatePair {
562
- return { local: { transport }, remote: { address: rinfo.address, port: rinfo.port } };
563
- }
564
-
565
- #select(pair: CandidatePair | undefined): void {
566
- if (this.#selected || !pair) return;
567
- this.#selected = pair;
568
- this.#connected = true;
569
- this.#stopChecks();
570
- this.emit('selected', {
571
- transport: pair.local.transport,
572
- candidateType: pair.local.type,
573
- remoteAddress: pair.remote.address,
574
- remotePort: pair.remote.port,
575
- });
576
- this.emit('connected');
577
- }
578
-
579
- /**
580
- * Send application (DTLS) data over the selected path.
581
- * @param {Buffer} data
582
- */
583
- send(data: Buffer): void {
584
- if (!this.#selected) throw new Error('ICE not connected');
585
- this.#selected.local.transport.send(data, this.#selected.remote.address, this.#selected.remote.port);
586
- }
587
-
588
- getSelectedPair(): CandidatePair | null {
589
- return this.#selected;
590
- }
591
-
592
- /** Type of the selected local candidate ('host'|'srflx'|'relay'|'prflx'). */
593
- getSelectedCandidateType(): string | null | undefined {
594
- return this.#selected ? this.#selected.local.type : null;
595
- }
596
-
597
- close(): void {
598
- if (this.#closed) return;
599
- this.#closed = true;
600
- this.#stopChecks();
601
- for (const t of this.#transports) {
602
- try { t.close(); } catch (_) {}
603
- }
604
- this.#transports = [];
605
- this.emit('closed');
606
- }
607
- }
608
-
609
- export { IceAgent, candidatePriority, parseIceServerUrl };