node-rtc-connection 2.0.4 → 2.0.6

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/types → types}/stun/stun-client.d.ts +2 -0
  5. package/dist/index.cjs +0 -32
  6. package/dist/index.mjs +0 -43
  7. package/src/crypto/der.ts +0 -205
  8. package/src/crypto/x509.ts +0 -146
  9. package/src/datachannel/RTCDataChannel.ts +0 -388
  10. package/src/dtls/RTCCertificate.ts +0 -396
  11. package/src/dtls/cipher.ts +0 -198
  12. package/src/dtls/connection.ts +0 -974
  13. package/src/dtls/prf.ts +0 -62
  14. package/src/dtls/protocol.ts +0 -204
  15. package/src/foundation/ByteBufferQueue.ts +0 -237
  16. package/src/foundation/RTCError.ts +0 -276
  17. package/src/ice/RTCIceCandidate.ts +0 -349
  18. package/src/ice/ice-agent.ts +0 -609
  19. package/src/ice/stun-message.ts +0 -260
  20. package/src/index.ts +0 -72
  21. package/src/peerconnection/RTCPeerConnection.ts +0 -430
  22. package/src/sctp/association.ts +0 -523
  23. package/src/sctp/chunks.ts +0 -350
  24. package/src/sctp/crc32c.ts +0 -57
  25. package/src/sctp/datachannel-manager.ts +0 -187
  26. package/src/sctp/dcep.ts +0 -94
  27. package/src/sdp/RTCSessionDescription.ts +0 -115
  28. package/src/sdp/sdp-utils.ts +0 -229
  29. package/src/stun/stun-client.ts +0 -936
  30. package/src/transport-stack.ts +0 -165
  31. /package/{dist/types → types}/crypto/der.d.ts +0 -0
  32. /package/{dist/types → types}/crypto/x509.d.ts +0 -0
  33. /package/{dist/types → types}/datachannel/RTCDataChannel.d.ts +0 -0
  34. /package/{dist/types → types}/dtls/RTCCertificate.d.ts +0 -0
  35. /package/{dist/types → types}/dtls/cipher.d.ts +0 -0
  36. /package/{dist/types → types}/dtls/connection.d.ts +0 -0
  37. /package/{dist/types → types}/dtls/prf.d.ts +0 -0
  38. /package/{dist/types → types}/dtls/protocol.d.ts +0 -0
  39. /package/{dist/types → types}/foundation/ByteBufferQueue.d.ts +0 -0
  40. /package/{dist/types → types}/foundation/RTCError.d.ts +0 -0
  41. /package/{dist/types → types}/ice/RTCIceCandidate.d.ts +0 -0
  42. /package/{dist/types → types}/ice/ice-agent.d.ts +0 -0
  43. /package/{dist/types → types}/ice/stun-message.d.ts +0 -0
  44. /package/{dist/types → types}/index.d.ts +0 -0
  45. /package/{dist/types → types}/peerconnection/RTCPeerConnection.d.ts +0 -0
  46. /package/{dist/types → types}/sctp/association.d.ts +0 -0
  47. /package/{dist/types → types}/sctp/chunks.d.ts +0 -0
  48. /package/{dist/types → types}/sctp/crc32c.d.ts +0 -0
  49. /package/{dist/types → types}/sctp/datachannel-manager.d.ts +0 -0
  50. /package/{dist/types → types}/sctp/dcep.d.ts +0 -0
  51. /package/{dist/types → types}/sdp/RTCSessionDescription.d.ts +0 -0
  52. /package/{dist/types → types}/sdp/sdp-utils.d.ts +0 -0
  53. /package/{dist/types → types}/transport-stack.d.ts +0 -0
@@ -1,430 +0,0 @@
1
- /**
2
- * @file RTCPeerConnection.ts
3
- * @description WebRTC peer connection driving a real ICE/DTLS/SCTP/DCEP stack.
4
- * @module peerconnection/RTCPeerConnection
5
- *
6
- * This orchestrates signaling (offer/answer + ICE candidate trickle) on top of
7
- * src/transport-stack.ts, which implements the actual on-the-wire protocols.
8
- * It interoperates with browser RTCPeerConnection for data channels.
9
- */
10
-
11
- 'use strict';
12
-
13
- import { EventEmitter } from 'events';
14
- import RTCCertificate from '../dtls/RTCCertificate';
15
- import { RTCSessionDescription, RTCSdpType, RTCSessionDescriptionInit } from '../sdp/RTCSessionDescription';
16
- import { RTCDataChannel, RTCDataChannelInit } from '../datachannel/RTCDataChannel';
17
- import * as sdpUtils from '../sdp/sdp-utils';
18
- import { IceCredentials, Fingerprint } from '../sdp/sdp-utils';
19
- import { TransportStack } from '../transport-stack';
20
- import type { OpenRequestInfo } from '../sctp/datachannel-manager';
21
-
22
- /** Configuration accepted by {@link RTCPeerConnection}. */
23
- export interface RTCConfiguration {
24
- iceServers?: unknown[];
25
- iceTransportPolicy?: 'all' | 'relay';
26
- certificates?: RTCCertificate[];
27
- }
28
-
29
- /** A queued local channel awaiting an established SCTP association. */
30
- interface PendingChannel {
31
- channel: RTCDataChannel;
32
- init: RTCDataChannelInit;
33
- }
34
-
35
- /** Candidate descriptor emitted by the ICE agent through the stack. */
36
- interface StackCandidate {
37
- sdp: string;
38
- }
39
-
40
- export const RTCSignalingState = Object.freeze({
41
- STABLE: 'stable',
42
- HAVE_LOCAL_OFFER: 'have-local-offer',
43
- HAVE_REMOTE_OFFER: 'have-remote-offer',
44
- HAVE_LOCAL_PRANSWER: 'have-local-pranswer',
45
- HAVE_REMOTE_PRANSWER: 'have-remote-pranswer',
46
- CLOSED: 'closed',
47
- });
48
-
49
- export const RTCIceGatheringState = Object.freeze({ NEW: 'new', GATHERING: 'gathering', COMPLETE: 'complete' });
50
-
51
- export const RTCPeerConnectionState = Object.freeze({
52
- NEW: 'new',
53
- CONNECTING: 'connecting',
54
- CONNECTED: 'connected',
55
- DISCONNECTED: 'disconnected',
56
- FAILED: 'failed',
57
- CLOSED: 'closed',
58
- });
59
-
60
- export class RTCPeerConnection extends EventEmitter {
61
- #configuration: RTCConfiguration;
62
- #signalingState: string;
63
- #iceGatheringState: string;
64
- #connectionState: string;
65
-
66
- #localDescription: RTCSessionDescription | null;
67
- #remoteDescription: RTCSessionDescription | null;
68
-
69
- #certificate: RTCCertificate | null;
70
- #localIce: IceCredentials;
71
- #remoteIce: sdpUtils.IceParameters | null;
72
- #remoteFingerprints: Fingerprint[];
73
- #remoteSetup: string | null;
74
-
75
- #stack: TransportStack | null;
76
- #isOfferer: boolean;
77
- #isClosed: boolean;
78
-
79
- #pendingChannels: PendingChannel[];
80
- #channels: Set<RTCDataChannel>;
81
- #localCandidates: RTCIceCandidateInit[];
82
-
83
- constructor(configuration: RTCConfiguration = {}) {
84
- super();
85
- this.#configuration = configuration;
86
- this.#signalingState = RTCSignalingState.STABLE;
87
- this.#iceGatheringState = RTCIceGatheringState.NEW;
88
- this.#connectionState = RTCPeerConnectionState.NEW;
89
-
90
- this.#localDescription = null;
91
- this.#remoteDescription = null;
92
-
93
- this.#certificate = null;
94
- this.#localIce = sdpUtils.generateIceCredentials();
95
- this.#remoteIce = null;
96
- this.#remoteFingerprints = [];
97
- this.#remoteSetup = null;
98
-
99
- this.#stack = null;
100
- this.#isOfferer = false;
101
- this.#isClosed = false;
102
-
103
- // Data channels created locally before the stack is ready are queued and
104
- // opened once SCTP is established.
105
- this.#pendingChannels = [];
106
- this.#channels = new Set();
107
- this.#localCandidates = [];
108
- }
109
-
110
- // ---- lazy init ----------------------------------------------------------
111
-
112
- async #ensureCertificate(): Promise<RTCCertificate> {
113
- if (!this.#certificate) {
114
- if (this.#configuration.certificates && this.#configuration.certificates[0]) {
115
- this.#certificate = this.#configuration.certificates[0];
116
- } else {
117
- this.#certificate = await RTCCertificate.generateCertificate();
118
- }
119
- }
120
- return this.#certificate;
121
- }
122
-
123
- /**
124
- * Create the transport stack once roles are known.
125
- * @param {'controlling'|'controlled'} iceRole
126
- * @param {'client'|'server'} dtlsRole
127
- */
128
- #createStack(iceRole: 'controlling' | 'controlled', dtlsRole: 'client' | 'server'): TransportStack {
129
- if (this.#stack) return this.#stack;
130
-
131
- const certificate = this.#certificate;
132
- if (!certificate) {
133
- throw new Error('Certificate not initialized');
134
- }
135
- const certDer = certificate.getCertificateDer();
136
- if (!certDer) {
137
- throw new Error('Certificate has no DER encoding');
138
- }
139
-
140
- const stack = new TransportStack({
141
- iceRole,
142
- dtlsRole,
143
- localUfrag: this.#localIce.usernameFragment,
144
- localPwd: this.#localIce.password,
145
- certDer,
146
- privateKey: certificate.getPrivateKeyObject(),
147
- verifyFingerprint: (fp: { algorithm: string; value: string }) => this.#verifyRemoteFingerprint(fp),
148
- });
149
-
150
- stack.on('candidate', (c: StackCandidate) => {
151
- const init: RTCIceCandidateInit = { candidate: c.sdp, sdpMid: '0', sdpMLineIndex: 0, usernameFragment: this.#localIce.usernameFragment };
152
- this.#localCandidates.push(init);
153
- this.emit('icecandidate', { candidate: init });
154
- });
155
-
156
- stack.on('iceconnected', () => this.#setConnectionState(RTCPeerConnectionState.CONNECTING));
157
- stack.on('sctpconnected', () => this.#setConnectionState(RTCPeerConnectionState.CONNECTED));
158
- stack.on('error', (e: unknown) => {
159
- this.emit('error', e);
160
- this.#setConnectionState(RTCPeerConnectionState.FAILED);
161
- });
162
- stack.on('close', () => this.#setConnectionState(RTCPeerConnectionState.DISCONNECTED));
163
-
164
- // Inbound (remotely-initiated) data channels.
165
- stack.on('datachannel-request', (info: OpenRequestInfo) => {
166
- const channel = new RTCDataChannel(info.label, {
167
- ordered: info.ordered,
168
- protocol: info.protocol,
169
- id: info.streamId,
170
- });
171
- stack.acceptChannel(channel, info);
172
- this.#channels.add(channel);
173
- this.emit('datachannel', { channel });
174
- });
175
-
176
- // Open any queued local channels when SCTP is ready.
177
- stack.on('ready', () => {
178
- for (const { channel, init } of this.#pendingChannels) {
179
- stack.openChannel(channel, init);
180
- }
181
- this.#pendingChannels = [];
182
- });
183
-
184
- this.#stack = stack;
185
- return stack;
186
- }
187
-
188
- #verifyRemoteFingerprint(fp: { algorithm: string; value: string }): boolean {
189
- if (this.#remoteFingerprints.length === 0) return true; // not yet known
190
- return this.#remoteFingerprints.some(
191
- (rf) => rf.algorithm === fp.algorithm && rf.value.toUpperCase() === fp.value.toUpperCase()
192
- );
193
- }
194
-
195
- // ---- data channels ------------------------------------------------------
196
-
197
- createDataChannel(label: string, options: RTCDataChannelInit = {}): RTCDataChannel {
198
- if (this.#isClosed) throw new Error('RTCPeerConnection is closed');
199
- const channel = new RTCDataChannel(label, options);
200
- this.#channels.add(channel);
201
-
202
- const init: RTCDataChannelInit = {
203
- ordered: options.ordered !== false,
204
- maxRetransmits: options.maxRetransmits,
205
- maxPacketLifeTime: options.maxPacketLifeTime,
206
- protocol: options.protocol || '',
207
- negotiated: options.negotiated || false,
208
- };
209
-
210
- if (this.#stack && this.#stack.isReady()) {
211
- this.#stack.openChannel(channel, init);
212
- } else {
213
- this.#pendingChannels.push({ channel, init });
214
- }
215
-
216
- setImmediate(() => { if (!this.#isClosed) this.emit('negotiationneeded'); });
217
- return channel;
218
- }
219
-
220
- // ---- signaling ----------------------------------------------------------
221
-
222
- async createOffer(): Promise<RTCSessionDescription> {
223
- if (this.#isClosed) throw new Error('RTCPeerConnection is closed');
224
- await this.#ensureCertificate();
225
- this.#isOfferer = true;
226
-
227
- const fp = this.#pickFingerprint();
228
- const sdp = sdpUtils.generateOffer({
229
- iceUfrag: this.#localIce.usernameFragment,
230
- icePwd: this.#localIce.password,
231
- fingerprint: fp,
232
- setup: 'actpass',
233
- candidates: [],
234
- });
235
- return new RTCSessionDescription({ type: RTCSdpType.OFFER, sdp });
236
- }
237
-
238
- async createAnswer(): Promise<RTCSessionDescription> {
239
- if (this.#isClosed) throw new Error('RTCPeerConnection is closed');
240
- if (!this.#remoteDescription || this.#remoteDescription.type !== 'offer') {
241
- throw new Error('Cannot create answer without remote offer');
242
- }
243
- await this.#ensureCertificate();
244
-
245
- // Answerer takes the active (DTLS client) role when offer is actpass.
246
- const fp = this.#pickFingerprint();
247
- const sdp = sdpUtils.generateAnswer({
248
- iceUfrag: this.#localIce.usernameFragment,
249
- icePwd: this.#localIce.password,
250
- fingerprint: fp,
251
- setup: 'active',
252
- candidates: [],
253
- });
254
- return new RTCSessionDescription({ type: RTCSdpType.ANSWER, sdp });
255
- }
256
-
257
- #pickFingerprint(): Fingerprint | undefined {
258
- const certificate = this.#certificate;
259
- if (!certificate) {
260
- throw new Error('Certificate not initialized');
261
- }
262
- const fps = certificate.getFingerprints();
263
- return fps.find((f) => f.algorithm === 'sha-256') || fps[0];
264
- }
265
-
266
- async setLocalDescription(description?: RTCSessionDescriptionInit | RTCSessionDescription): Promise<void> {
267
- if (this.#isClosed) throw new Error('RTCPeerConnection is closed');
268
- const desc: RTCSessionDescriptionInit | RTCSessionDescription = description
269
- ? description
270
- : this.#signalingState === RTCSignalingState.HAVE_REMOTE_OFFER
271
- ? await this.createAnswer()
272
- : await this.createOffer();
273
- await this.#ensureCertificate();
274
- this.#localDescription = new RTCSessionDescription({ type: desc.type ?? undefined, sdp: desc.sdp ?? undefined });
275
-
276
- if (desc.type === 'offer') {
277
- this.#signalingState = RTCSignalingState.HAVE_LOCAL_OFFER;
278
- } else if (desc.type === 'answer') {
279
- this.#signalingState = RTCSignalingState.STABLE;
280
- }
281
-
282
- // Determine roles and bring up the stack so we start gathering immediately.
283
- this.#setupRolesAndStack(desc, /*local*/ true);
284
-
285
- this.#iceGatheringState = RTCIceGatheringState.GATHERING;
286
- this.emit('icegatheringstatechange');
287
- if (this.#stack) {
288
- await this.#stack.gather({
289
- iceServers: this.#configuration.iceServers || [],
290
- iceTransportPolicy: this.#configuration.iceTransportPolicy || 'all',
291
- });
292
- this.#iceGatheringState = RTCIceGatheringState.COMPLETE;
293
- this.emit('icegatheringstatechange');
294
- // Signal end-of-candidates.
295
- this.emit('icecandidate', { candidate: null });
296
- this.#maybeStartChecks();
297
- }
298
- this.emit('signalingstatechange');
299
- }
300
-
301
- async setRemoteDescription(description: RTCSessionDescriptionInit): Promise<void> {
302
- if (this.#isClosed) throw new Error('RTCPeerConnection is closed');
303
- if (!description || !description.sdp) throw new Error('Invalid session description');
304
- await this.#ensureCertificate();
305
-
306
- this.#remoteDescription = new RTCSessionDescription(description);
307
- if (description.type === 'offer') {
308
- this.#signalingState = RTCSignalingState.HAVE_REMOTE_OFFER;
309
- } else if (description.type === 'answer') {
310
- this.#signalingState = RTCSignalingState.STABLE;
311
- }
312
-
313
- this.#remoteIce = sdpUtils.parseIceParameters(description.sdp);
314
- const dtls = sdpUtils.parseDtlsParameters(description.sdp);
315
- this.#remoteFingerprints = dtls.fingerprints;
316
- this.#remoteSetup = dtls.setup ?? null;
317
-
318
- this.#setupRolesAndStack(description, /*local*/ false);
319
-
320
- // Apply remote credentials + any in-SDP candidates, then start checks.
321
- if (this.#stack && this.#remoteIce.usernameFragment) {
322
- this.#stack.setRemote(this.#remoteIce.usernameFragment, this.#remoteIce.password ?? '');
323
- for (const c of sdpUtils.parseCandidates(description.sdp)) {
324
- this.#stack.addRemoteCandidate(c);
325
- }
326
- }
327
- this.#maybeStartChecks();
328
- this.emit('signalingstatechange');
329
- }
330
-
331
- /**
332
- * Decide ICE controlling/controlled and DTLS client/server, then create the
333
- * stack. Offerer is ICE-controlling. DTLS roles follow a=setup: the side that
334
- * ends up 'active' is the DTLS client.
335
- */
336
- #setupRolesAndStack(_description: RTCSessionDescriptionInit | RTCSessionDescription, _isLocal: boolean): void {
337
- if (this.#stack) return;
338
-
339
- const iceRole: 'controlling' | 'controlled' = this.#isOfferer ? 'controlling' : 'controlled';
340
-
341
- // Determine our DTLS role.
342
- let dtlsRole: 'client' | 'server';
343
- if (this.#isOfferer) {
344
- // We offered actpass; the answerer chooses. We learn it from their answer
345
- // (setup:active => they are client => we are server). Until we see the
346
- // answer we default to server (passive), which matches answerer=active.
347
- if (this.#remoteSetup === 'active') dtlsRole = 'server';
348
- else if (this.#remoteSetup === 'passive') dtlsRole = 'client';
349
- else dtlsRole = 'server';
350
- } else {
351
- // We are the answerer; we chose 'active' in createAnswer => DTLS client.
352
- dtlsRole = 'client';
353
- }
354
-
355
- this.#createStack(iceRole, dtlsRole);
356
- }
357
-
358
- #maybeStartChecks(): void {
359
- // Once both local gathering started and remote creds exist, checks run.
360
- if (this.#stack && this.#remoteIce && this.#remoteIce.usernameFragment) {
361
- this.#stack.setRemote(this.#remoteIce.usernameFragment, this.#remoteIce.password ?? '');
362
- }
363
- }
364
-
365
- async addIceCandidate(candidate: RTCIceCandidateInit | string): Promise<void> {
366
- if (this.#isClosed) throw new Error('RTCPeerConnection is closed');
367
- if (!candidate || (typeof candidate !== 'string' && candidate.candidate === '')) {
368
- return; // end-of-candidates
369
- }
370
- const candidateStr = typeof candidate === 'string' ? candidate : (candidate.candidate || '');
371
- const parsed = sdpUtils.parseCandidateLine(candidateStr);
372
- if (parsed && this.#stack) {
373
- this.#stack.addRemoteCandidate(parsed);
374
- }
375
- }
376
-
377
- // ---- state --------------------------------------------------------------
378
-
379
- #setConnectionState(state: string): void {
380
- if (this.#connectionState !== state) {
381
- this.#connectionState = state;
382
- this.emit('connectionstatechange');
383
- this.emit('iceconnectionstatechange');
384
- }
385
- }
386
-
387
- getConfiguration(): RTCConfiguration { return { ...this.#configuration }; }
388
- setConfiguration(configuration: RTCConfiguration): void {
389
- if (this.#isClosed) throw new Error('RTCPeerConnection is closed');
390
- this.#configuration = { ...configuration };
391
- }
392
-
393
- close(): void {
394
- if (this.#isClosed) return;
395
- this.#isClosed = true;
396
- this.#signalingState = RTCSignalingState.CLOSED;
397
- for (const channel of this.#channels) {
398
- try { channel.close(); } catch (_) { /* best-effort */ }
399
- }
400
- if (this.#stack) try { this.#stack.close(); } catch (_) { /* best-effort */ }
401
- this.#setConnectionState(RTCPeerConnectionState.CLOSED);
402
- this.emit('signalingstatechange');
403
- }
404
-
405
- get signalingState(): string { return this.#signalingState; }
406
- get iceGatheringState(): string { return this.#iceGatheringState; }
407
- get iceConnectionState(): string {
408
- return this.#connectionState === RTCPeerConnectionState.CONNECTED ? 'connected'
409
- : this.#connectionState === RTCPeerConnectionState.CONNECTING ? 'checking'
410
- : this.#connectionState === RTCPeerConnectionState.FAILED ? 'failed'
411
- : 'new';
412
- }
413
- get connectionState(): string { return this.#connectionState; }
414
- get localDescription(): RTCSessionDescription | null { return this.#localDescription; }
415
- get remoteDescription(): RTCSessionDescription | null { return this.#remoteDescription; }
416
- get currentLocalDescription(): RTCSessionDescription | null { return this.#localDescription; }
417
- get currentRemoteDescription(): RTCSessionDescription | null { return this.#remoteDescription; }
418
- get pendingLocalDescription(): RTCSessionDescription | null { return this.#signalingState === RTCSignalingState.STABLE ? null : this.#localDescription; }
419
- get pendingRemoteDescription(): RTCSessionDescription | null { return this.#signalingState === RTCSignalingState.STABLE ? null : this.#remoteDescription; }
420
- get canTrickleIceCandidates(): boolean { return true; }
421
- get sctp(): TransportStack['sctp'] { return this.#stack ? this.#stack.sctp : null; }
422
- }
423
-
424
- /** ICE candidate init shape (subset of the W3C dictionary). */
425
- interface RTCIceCandidateInit {
426
- candidate: string;
427
- sdpMid?: string;
428
- sdpMLineIndex?: number;
429
- usernameFragment?: string;
430
- }