node-rtc-connection 1.0.19 → 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 (64) hide show
  1. package/README.md +94 -85
  2. package/dist/index.cjs +20 -5606
  3. package/dist/index.mjs +25 -5598
  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.js → stun-client.ts} +346 -187
  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 -1018
  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 -875
  63. package/src/sctp/RTCSctpTransport.js +0 -253
  64. package/src/sdp/sdp-utils.js +0 -224
@@ -0,0 +1,430 @@
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
+ }