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
@@ -0,0 +1,350 @@
1
+ /**
2
+ * @file chunks.ts
3
+ * @description SCTP common header and chunk encode/parse (RFC 4960 + RFC 8260
4
+ * for I-DATA is NOT used; classic DATA only). Scoped to the WebRTC profile.
5
+ * @module sctp/chunks
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ export const CHUNK_TYPE = Object.freeze({
11
+ DATA: 0,
12
+ INIT: 1,
13
+ INIT_ACK: 2,
14
+ SACK: 3,
15
+ HEARTBEAT: 4,
16
+ HEARTBEAT_ACK: 5,
17
+ ABORT: 6,
18
+ SHUTDOWN: 7,
19
+ SHUTDOWN_ACK: 8,
20
+ ERROR: 9,
21
+ COOKIE_ECHO: 10,
22
+ COOKIE_ACK: 11,
23
+ SHUTDOWN_COMPLETE: 14,
24
+ FORWARD_TSN: 192, // 0xC0
25
+ });
26
+
27
+ export const PARAM_TYPE = Object.freeze({
28
+ HEARTBEAT_INFO: 1,
29
+ STATE_COOKIE: 7,
30
+ UNRECOGNIZED_PARAM: 8,
31
+ COOKIE_PRESERVATIVE: 9,
32
+ SUPPORTED_ADDR_TYPES: 11,
33
+ // RFC 8260 / RFC 3758
34
+ FORWARD_TSN_SUPPORTED: 49152, // 0xC000
35
+ SUPPORTED_EXTENSIONS: 32776, // 0x8008
36
+ });
37
+
38
+ // Payload Protocol Identifiers used by WebRTC data channels (RFC 8831).
39
+ export const PPID = Object.freeze({
40
+ DCEP: 50,
41
+ STRING: 51,
42
+ BINARY: 53,
43
+ STRING_EMPTY: 56,
44
+ BINARY_EMPTY: 57,
45
+ STRING_PARTIAL: 54, // deprecated
46
+ BINARY_PARTIAL: 52, // deprecated
47
+ });
48
+
49
+ export interface CommonHeader {
50
+ srcPort: number;
51
+ dstPort: number;
52
+ verificationTag: number;
53
+ checksum: number;
54
+ }
55
+
56
+ export interface ParsedChunk {
57
+ type: number;
58
+ flags: number;
59
+ length: number;
60
+ body: Buffer;
61
+ }
62
+
63
+ export interface ParsedParam {
64
+ type: number;
65
+ length: number;
66
+ value: Buffer;
67
+ }
68
+
69
+ export interface InitBodyParams {
70
+ initiateTag: number;
71
+ a_rwnd: number;
72
+ outStreams: number;
73
+ inStreams: number;
74
+ initialTSN: number;
75
+ }
76
+
77
+ export interface ParsedInitBody {
78
+ initiateTag: number;
79
+ a_rwnd: number;
80
+ outStreams: number;
81
+ inStreams: number;
82
+ initialTSN: number;
83
+ params: ParsedParam[];
84
+ }
85
+
86
+ export interface DataBodyParams {
87
+ tsn: number;
88
+ streamId: number;
89
+ streamSeq: number;
90
+ ppid: number;
91
+ userData: Buffer;
92
+ unordered?: boolean;
93
+ beginning?: boolean;
94
+ ending?: boolean;
95
+ }
96
+
97
+ export interface EncodedDataBody {
98
+ flags: number;
99
+ body: Buffer;
100
+ }
101
+
102
+ export interface ParsedDataBody {
103
+ unordered: boolean;
104
+ beginning: boolean;
105
+ ending: boolean;
106
+ tsn: number;
107
+ streamId: number;
108
+ streamSeq: number;
109
+ ppid: number;
110
+ userData: Buffer;
111
+ }
112
+
113
+ export interface SackBodyParams {
114
+ cumulativeTSNAck: number;
115
+ a_rwnd: number;
116
+ gapBlocks?: Array<[number, number]>;
117
+ dupTSNs?: number[];
118
+ }
119
+
120
+ export interface ParsedSackBody {
121
+ cumulativeTSNAck: number;
122
+ a_rwnd: number;
123
+ gapBlocks: Array<[number, number]>;
124
+ dupTSNs: number[];
125
+ }
126
+
127
+ /** Round a length up to the next 4-byte boundary. */
128
+ export function pad4(n: number): number {
129
+ return (n + 3) & ~3;
130
+ }
131
+
132
+ /**
133
+ * Encode the 12-byte SCTP common header (checksum left as 0; filled by crc32c).
134
+ * @param {number} srcPort
135
+ * @param {number} dstPort
136
+ * @param {number} verificationTag
137
+ * @returns {Buffer}
138
+ */
139
+ export function encodeCommonHeader(srcPort: number, dstPort: number, verificationTag: number): Buffer {
140
+ const h = Buffer.alloc(12);
141
+ h.writeUInt16BE(srcPort, 0);
142
+ h.writeUInt16BE(dstPort, 2);
143
+ h.writeUInt32BE(verificationTag >>> 0, 4);
144
+ h.writeUInt32BE(0, 8); // checksum placeholder
145
+ return h;
146
+ }
147
+
148
+ /**
149
+ * Parse the common header.
150
+ * @param {Buffer} packet
151
+ * @returns {{srcPort:number,dstPort:number,verificationTag:number,checksum:number}}
152
+ */
153
+ export function parseCommonHeader(packet: Buffer): CommonHeader {
154
+ return {
155
+ srcPort: packet.readUInt16BE(0),
156
+ dstPort: packet.readUInt16BE(2),
157
+ verificationTag: packet.readUInt32BE(4),
158
+ checksum: packet.readUInt32LE(8),
159
+ };
160
+ }
161
+
162
+ /**
163
+ * Wrap a chunk body with the 4-byte chunk header, padded to 4 bytes.
164
+ * @param {number} type
165
+ * @param {number} flags
166
+ * @param {Buffer} body
167
+ * @returns {Buffer}
168
+ */
169
+ export function encodeChunk(type: number, flags: number, body: Buffer): Buffer {
170
+ const len = 4 + body.length;
171
+ const out = Buffer.alloc(pad4(len));
172
+ out.writeUInt8(type, 0);
173
+ out.writeUInt8(flags, 1);
174
+ out.writeUInt16BE(len, 2); // length excludes padding
175
+ body.copy(out, 4);
176
+ return out;
177
+ }
178
+
179
+ /**
180
+ * Parse all chunks out of an SCTP packet (after the 12-byte common header).
181
+ * @param {Buffer} packet
182
+ * @returns {Array<{type:number,flags:number,length:number,body:Buffer}>}
183
+ */
184
+ export function parseChunks(packet: Buffer): ParsedChunk[] {
185
+ const chunks: ParsedChunk[] = [];
186
+ let off = 12;
187
+ while (off + 4 <= packet.length) {
188
+ const type = packet.readUInt8(off);
189
+ const flags = packet.readUInt8(off + 1);
190
+ const length = packet.readUInt16BE(off + 2);
191
+ if (length < 4 || off + length > packet.length) break;
192
+ const body = packet.slice(off + 4, off + length);
193
+ chunks.push({ type, flags, length, body });
194
+ off += pad4(length);
195
+ }
196
+ return chunks;
197
+ }
198
+
199
+ /**
200
+ * Encode a TLV parameter, padded to 4 bytes.
201
+ * @param {number} type
202
+ * @param {Buffer} value
203
+ * @returns {Buffer}
204
+ */
205
+ export function encodeParam(type: number, value: Buffer): Buffer {
206
+ const len = 4 + value.length;
207
+ const out = Buffer.alloc(pad4(len));
208
+ out.writeUInt16BE(type, 0);
209
+ out.writeUInt16BE(len, 2);
210
+ value.copy(out, 4);
211
+ return out;
212
+ }
213
+
214
+ /**
215
+ * Parse TLV parameters from a buffer.
216
+ * @param {Buffer} buf
217
+ * @returns {Array<{type:number,length:number,value:Buffer}>}
218
+ */
219
+ export function parseParams(buf: Buffer): ParsedParam[] {
220
+ const params: ParsedParam[] = [];
221
+ let off = 0;
222
+ while (off + 4 <= buf.length) {
223
+ const type = buf.readUInt16BE(off);
224
+ const length = buf.readUInt16BE(off + 2);
225
+ if (length < 4 || off + length > buf.length) break;
226
+ params.push({ type, length, value: buf.slice(off + 4, off + length) });
227
+ off += pad4(length);
228
+ }
229
+ return params;
230
+ }
231
+
232
+ /**
233
+ * Build an INIT or INIT_ACK fixed body (without parameters).
234
+ * @param {Object} p
235
+ * @param {number} p.initiateTag
236
+ * @param {number} p.a_rwnd - advertised receiver window
237
+ * @param {number} p.outStreams
238
+ * @param {number} p.inStreams
239
+ * @param {number} p.initialTSN
240
+ * @returns {Buffer}
241
+ */
242
+ export function encodeInitBody({ initiateTag, a_rwnd, outStreams, inStreams, initialTSN }: InitBodyParams): Buffer {
243
+ const b = Buffer.alloc(16);
244
+ b.writeUInt32BE(initiateTag >>> 0, 0);
245
+ b.writeUInt32BE(a_rwnd >>> 0, 4);
246
+ b.writeUInt16BE(outStreams, 8);
247
+ b.writeUInt16BE(inStreams, 10);
248
+ b.writeUInt32BE(initialTSN >>> 0, 12);
249
+ return b;
250
+ }
251
+
252
+ /**
253
+ * Parse an INIT/INIT_ACK body.
254
+ * @param {Buffer} body
255
+ * @returns {{initiateTag:number,a_rwnd:number,outStreams:number,inStreams:number,initialTSN:number,params:Array}}
256
+ */
257
+ export function parseInitBody(body: Buffer): ParsedInitBody {
258
+ return {
259
+ initiateTag: body.readUInt32BE(0),
260
+ a_rwnd: body.readUInt32BE(4),
261
+ outStreams: body.readUInt16BE(8),
262
+ inStreams: body.readUInt16BE(10),
263
+ initialTSN: body.readUInt32BE(12),
264
+ params: parseParams(body.slice(16)),
265
+ };
266
+ }
267
+
268
+ /**
269
+ * Encode a DATA chunk body (RFC 4960 §3.3.1).
270
+ * @param {Object} p
271
+ * @param {number} p.tsn
272
+ * @param {number} p.streamId
273
+ * @param {number} p.streamSeq
274
+ * @param {number} p.ppid
275
+ * @param {Buffer} p.userData
276
+ * @returns {{flags:number, body:Buffer}}
277
+ */
278
+ export function encodeDataBody({ tsn, streamId, streamSeq, ppid, userData, unordered = false, beginning = true, ending = true }: DataBodyParams): EncodedDataBody {
279
+ const head = Buffer.alloc(12);
280
+ head.writeUInt32BE(tsn >>> 0, 0);
281
+ head.writeUInt16BE(streamId, 4);
282
+ head.writeUInt16BE(streamSeq, 6);
283
+ head.writeUInt32BE(ppid >>> 0, 8);
284
+ let flags = 0;
285
+ if (ending) flags |= 0x01; // E
286
+ if (beginning) flags |= 0x02; // B
287
+ if (unordered) flags |= 0x04; // U
288
+ return { flags, body: Buffer.concat([head, userData]) };
289
+ }
290
+
291
+ /**
292
+ * Parse a DATA chunk body.
293
+ * @param {number} flags
294
+ * @param {Buffer} body
295
+ */
296
+ export function parseDataBody(flags: number, body: Buffer): ParsedDataBody {
297
+ return {
298
+ unordered: !!(flags & 0x04),
299
+ beginning: !!(flags & 0x02),
300
+ ending: !!(flags & 0x01),
301
+ tsn: body.readUInt32BE(0),
302
+ streamId: body.readUInt16BE(4),
303
+ streamSeq: body.readUInt16BE(6),
304
+ ppid: body.readUInt32BE(8),
305
+ userData: body.slice(12),
306
+ };
307
+ }
308
+
309
+ /**
310
+ * Encode a SACK chunk body (cumulative ack only, no gap/dup for simplicity but
311
+ * gap blocks supported via params).
312
+ * @param {Object} p
313
+ * @param {number} p.cumulativeTSNAck
314
+ * @param {number} p.a_rwnd
315
+ * @param {Array<[number,number]>} [p.gapBlocks] - [start,end] offsets from cumAck+1
316
+ * @param {Array<number>} [p.dupTSNs]
317
+ * @returns {Buffer}
318
+ */
319
+ export function encodeSackBody({ cumulativeTSNAck, a_rwnd, gapBlocks = [], dupTSNs = [] }: SackBodyParams): Buffer {
320
+ const b = Buffer.alloc(12 + gapBlocks.length * 4 + dupTSNs.length * 4);
321
+ b.writeUInt32BE(cumulativeTSNAck >>> 0, 0);
322
+ b.writeUInt32BE(a_rwnd >>> 0, 4);
323
+ b.writeUInt16BE(gapBlocks.length, 8);
324
+ b.writeUInt16BE(dupTSNs.length, 10);
325
+ let o = 12;
326
+ for (const [start, end] of gapBlocks) {
327
+ b.writeUInt16BE(start, o); b.writeUInt16BE(end, o + 2); o += 4;
328
+ }
329
+ for (const d of dupTSNs) { b.writeUInt32BE(d >>> 0, o); o += 4; }
330
+ return b;
331
+ }
332
+
333
+ /**
334
+ * Parse a SACK chunk body.
335
+ */
336
+ export function parseSackBody(body: Buffer): ParsedSackBody {
337
+ const cumulativeTSNAck = body.readUInt32BE(0);
338
+ const a_rwnd = body.readUInt32BE(4);
339
+ const numGap = body.readUInt16BE(8);
340
+ const numDup = body.readUInt16BE(10);
341
+ const gapBlocks: Array<[number, number]> = [];
342
+ let o = 12;
343
+ for (let i = 0; i < numGap; i++) {
344
+ gapBlocks.push([body.readUInt16BE(o), body.readUInt16BE(o + 2)]);
345
+ o += 4;
346
+ }
347
+ const dupTSNs: number[] = [];
348
+ for (let i = 0; i < numDup; i++) { dupTSNs.push(body.readUInt32BE(o)); o += 4; }
349
+ return { cumulativeTSNAck, a_rwnd, gapBlocks, dupTSNs };
350
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * @file crc32c.ts
3
+ * @description CRC-32C (Castagnoli) checksum for SCTP packets (RFC 4960 App. B,
4
+ * polynomial 0x1EDC6F41), reflected input/output, used with the SCTP-specific
5
+ * byte ordering described in RFC 4960 §6.8 / RFC 3309.
6
+ * @module sctp/crc32c
7
+ */
8
+
9
+ // Precomputed reflected table for polynomial 0x1EDC6F41 (reflected 0x82F63B78).
10
+ const TABLE: Uint32Array = (() => {
11
+ const t = new Uint32Array(256);
12
+ for (let n = 0; n < 256; n++) {
13
+ let c = n;
14
+ for (let k = 0; k < 8; k++) {
15
+ c = c & 1 ? 0x82f63b78 ^ (c >>> 1) : c >>> 1;
16
+ }
17
+ t[n] = c >>> 0;
18
+ }
19
+ return t;
20
+ })();
21
+
22
+ /**
23
+ * Compute the raw reflected CRC-32C over a buffer.
24
+ * @returns unsigned 32-bit
25
+ */
26
+ export function crc32c(buf: Buffer): number {
27
+ let crc = 0xffffffff;
28
+ for (let i = 0; i < buf.length; i++) {
29
+ crc = TABLE[(crc ^ buf[i]!) & 0xff]! ^ (crc >>> 8);
30
+ }
31
+ return (crc ^ 0xffffffff) >>> 0;
32
+ }
33
+
34
+ /**
35
+ * Insert the SCTP checksum into a packet. The checksum field (bytes 8..11 of
36
+ * the common header) is zeroed, the CRC computed over the whole packet, then
37
+ * written back in little-endian byte order (RFC 4960 §6.8, RFC 3309).
38
+ * @param packet - full SCTP packet (header + chunks)
39
+ * @returns the same packet, checksum filled in
40
+ */
41
+ export function applyChecksum(packet: Buffer): Buffer {
42
+ packet.writeUInt32LE(0, 8);
43
+ const crc = crc32c(packet);
44
+ packet.writeUInt32LE(crc, 8);
45
+ return packet;
46
+ }
47
+
48
+ /**
49
+ * Validate the checksum of a received SCTP packet.
50
+ */
51
+ export function verifyChecksum(packet: Buffer): boolean {
52
+ const original = packet.readUInt32LE(8);
53
+ packet.writeUInt32LE(0, 8);
54
+ const crc = crc32c(packet);
55
+ packet.writeUInt32LE(original, 8); // restore
56
+ return crc === original;
57
+ }
@@ -0,0 +1,187 @@
1
+ /**
2
+ * @file datachannel-manager.ts
3
+ * @description Bridges SCTP streams + DCEP to RTCDataChannel instances.
4
+ * @module sctp/datachannel-manager
5
+ *
6
+ * Responsibilities:
7
+ * - Allocate SCTP stream IDs per RFC 8832 §6: the DTLS client (a=setup:active)
8
+ * uses even stream IDs, the DTLS server uses odd.
9
+ * - Send DATA_CHANNEL_OPEN and await DATA_CHANNEL_ACK; respond to inbound OPEN.
10
+ * - Map outgoing string/binary sends to the correct PPID, and incoming PPIDs
11
+ * back to string/binary, including the EMPTY variants for zero-length data.
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ import { EventEmitter } from 'events';
17
+ import * as dcep from './dcep';
18
+ import { PPID } from './chunks';
19
+ import type { SctpAssociation, SctpMessage } from './association';
20
+ import { RTCDataChannel, RTCDataChannelEvents } from '../datachannel/RTCDataChannel';
21
+
22
+ /** Subset of RTCDataChannelInit used when opening/accepting channels. */
23
+ interface ChannelInit {
24
+ ordered?: boolean;
25
+ maxRetransmits?: number | null;
26
+ maxPacketLifeTime?: number | null;
27
+ protocol?: string;
28
+ }
29
+
30
+ /** A locally tracked channel and whether its DCEP open has been acked. */
31
+ interface ChannelEntry {
32
+ channel: RTCDataChannel;
33
+ acked: boolean;
34
+ }
35
+
36
+ /** Information surfaced on the 'open-request' event for an inbound channel. */
37
+ export interface OpenRequestInfo {
38
+ streamId: number;
39
+ label: string;
40
+ protocol: string;
41
+ ordered: boolean;
42
+ channelType: number;
43
+ reliabilityParameter: number;
44
+ }
45
+
46
+ class DataChannelManager extends EventEmitter {
47
+ #sctp: SctpAssociation;
48
+ #channels: Map<number, ChannelEntry>; // streamId -> { channel, acked }
49
+ #nextStreamId: number;
50
+
51
+ /**
52
+ * @param {import('./association').SctpAssociation} association
53
+ * @param {boolean} isDtlsClient - true if we are the DTLS client (even IDs)
54
+ */
55
+ constructor(association: SctpAssociation, isDtlsClient: boolean) {
56
+ super();
57
+ this.#sctp = association;
58
+ this.#channels = new Map(); // streamId -> { channel, acked }
59
+ this.#nextStreamId = isDtlsClient ? 0 : 1;
60
+
61
+ this.#sctp.on('message', (m: SctpMessage) => this.#onSctpMessage(m));
62
+ }
63
+
64
+ /**
65
+ * Open a channel initiated locally.
66
+ * @param {import('../datachannel/RTCDataChannel').RTCDataChannel} channel
67
+ * @param {Object} init - { ordered, maxRetransmits, maxPacketLifeTime, protocol }
68
+ */
69
+ openChannel(channel: RTCDataChannel, init: ChannelInit = {}): void {
70
+ let streamId = channel.id;
71
+ if (streamId === null || streamId === undefined) {
72
+ streamId = this.#allocateStreamId();
73
+ channel.emit(RTCDataChannelEvents.SET_ID, streamId);
74
+ }
75
+ this.#channels.set(streamId, { channel, acked: false });
76
+ this.#attachSender(channel, streamId, init);
77
+
78
+ if (!channel.negotiated) {
79
+ const open = dcep.encodeOpen({
80
+ channelType: this.#channelType(init),
81
+ priority: 0,
82
+ reliabilityParameter: this.#reliabilityParam(init),
83
+ label: channel.label,
84
+ protocol: init.protocol || channel.protocol || '',
85
+ });
86
+ this.#sctp.sendData(streamId, PPID.DCEP, open);
87
+ // Negotiated=false channels open after receiving DATA_CHANNEL_ACK.
88
+ } else {
89
+ // Pre-negotiated: considered open immediately.
90
+ (this.#channels.get(streamId) as ChannelEntry).acked = true;
91
+ channel.emit(RTCDataChannelEvents.OPEN);
92
+ }
93
+ }
94
+
95
+ #allocateStreamId(): number {
96
+ let id = this.#nextStreamId;
97
+ while (this.#channels.has(id)) id += 2;
98
+ this.#nextStreamId = id + 2;
99
+ return id;
100
+ }
101
+
102
+ #channelType(init: ChannelInit): number {
103
+ const unordered = init.ordered === false;
104
+ if (init.maxRetransmits != null) {
105
+ return unordered
106
+ ? dcep.CHANNEL_TYPE.PARTIAL_RELIABLE_REXMIT_UNORDERED
107
+ : dcep.CHANNEL_TYPE.PARTIAL_RELIABLE_REXMIT;
108
+ }
109
+ if (init.maxPacketLifeTime != null) {
110
+ return unordered
111
+ ? dcep.CHANNEL_TYPE.PARTIAL_RELIABLE_TIMED_UNORDERED
112
+ : dcep.CHANNEL_TYPE.PARTIAL_RELIABLE_TIMED;
113
+ }
114
+ return unordered ? dcep.CHANNEL_TYPE.RELIABLE_UNORDERED : dcep.CHANNEL_TYPE.RELIABLE;
115
+ }
116
+
117
+ #reliabilityParam(init: ChannelInit): number {
118
+ if (init.maxRetransmits != null) return init.maxRetransmits >>> 0;
119
+ if (init.maxPacketLifeTime != null) return init.maxPacketLifeTime >>> 0;
120
+ return 0;
121
+ }
122
+
123
+ /** Wire the channel's outbound SEND events -> SCTP DATA with the right PPID. */
124
+ #attachSender(channel: RTCDataChannel, streamId: number, init: ChannelInit): void {
125
+ const unordered = init.ordered === false;
126
+ channel.on(RTCDataChannelEvents.SEND, (data: Buffer, isBinary: boolean) => {
127
+ let ppid: number;
128
+ if (isBinary) {
129
+ ppid = data.length === 0 ? PPID.BINARY_EMPTY : PPID.BINARY;
130
+ } else {
131
+ ppid = data.length === 0 ? PPID.STRING_EMPTY : PPID.STRING;
132
+ }
133
+ // EMPTY PPIDs still need one byte on the wire (RFC 8831 §6.6).
134
+ const payload = data.length === 0 ? Buffer.from([0]) : data;
135
+ this.#sctp.sendData(streamId, ppid, payload, { unordered });
136
+ });
137
+ }
138
+
139
+ #onSctpMessage(m: SctpMessage): void {
140
+ if (m.ppid === PPID.DCEP) {
141
+ this.#onDcep(m);
142
+ return;
143
+ }
144
+ const entry = this.#channels.get(m.streamId);
145
+ if (!entry) return; // data for unknown channel
146
+ const isBinary = m.ppid === PPID.BINARY || m.ppid === PPID.BINARY_EMPTY || m.ppid === PPID.BINARY_PARTIAL;
147
+ const isEmpty = m.ppid === PPID.STRING_EMPTY || m.ppid === PPID.BINARY_EMPTY;
148
+ const data = isEmpty ? Buffer.alloc(0) : m.data;
149
+ entry.channel.emit(RTCDataChannelEvents.RECEIVE, data, isBinary);
150
+ }
151
+
152
+ #onDcep(m: SctpMessage): void {
153
+ const type = dcep.messageType(m.data);
154
+ if (type === dcep.MESSAGE_TYPE.DATA_CHANNEL_OPEN) {
155
+ const open = dcep.decodeOpen(m.data);
156
+ // Acknowledge and surface a new inbound channel.
157
+ this.#sctp.sendData(m.streamId, PPID.DCEP, dcep.encodeAck());
158
+ this.emit('open-request', {
159
+ streamId: m.streamId,
160
+ label: open.label,
161
+ protocol: open.protocol,
162
+ ordered: !open.unordered,
163
+ channelType: open.channelType,
164
+ reliabilityParameter: open.reliabilityParameter,
165
+ });
166
+ } else if (type === dcep.MESSAGE_TYPE.DATA_CHANNEL_ACK) {
167
+ const entry = this.#channels.get(m.streamId);
168
+ if (entry && !entry.acked) {
169
+ entry.acked = true;
170
+ entry.channel.emit(RTCDataChannelEvents.OPEN);
171
+ }
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Register an inbound channel (created in response to 'open-request') and
177
+ * attach its sender.
178
+ */
179
+ acceptChannel(channel: RTCDataChannel, info: OpenRequestInfo): void {
180
+ channel.emit(RTCDataChannelEvents.SET_ID, info.streamId);
181
+ this.#channels.set(info.streamId, { channel, acked: true });
182
+ this.#attachSender(channel, info.streamId, { ordered: info.ordered });
183
+ channel.emit(RTCDataChannelEvents.OPEN);
184
+ }
185
+ }
186
+
187
+ export { DataChannelManager };
@@ -0,0 +1,94 @@
1
+ /**
2
+ * @file dcep.ts
3
+ * @description Data Channel Establishment Protocol (RFC 8832) message codec.
4
+ * @module sctp/dcep
5
+ *
6
+ * DCEP runs on PPID 50 and negotiates a data channel on an SCTP stream:
7
+ * DATA_CHANNEL_OPEN (0x03) -> DATA_CHANNEL_ACK (0x02)
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ export const MESSAGE_TYPE = Object.freeze({
13
+ DATA_CHANNEL_ACK: 0x02,
14
+ DATA_CHANNEL_OPEN: 0x03,
15
+ });
16
+
17
+ // Channel types (reliability/ordering), RFC 8832 §5.1.
18
+ export const CHANNEL_TYPE = Object.freeze({
19
+ RELIABLE: 0x00,
20
+ RELIABLE_UNORDERED: 0x80,
21
+ PARTIAL_RELIABLE_REXMIT: 0x01,
22
+ PARTIAL_RELIABLE_REXMIT_UNORDERED: 0x81,
23
+ PARTIAL_RELIABLE_TIMED: 0x02,
24
+ PARTIAL_RELIABLE_TIMED_UNORDERED: 0x82,
25
+ });
26
+
27
+ export interface OpenParams {
28
+ channelType: number;
29
+ priority?: number;
30
+ reliabilityParameter?: number;
31
+ label?: string;
32
+ protocol?: string;
33
+ }
34
+
35
+ export interface DecodedOpen {
36
+ channelType: number;
37
+ priority: number;
38
+ reliabilityParameter: number;
39
+ label: string;
40
+ protocol: string;
41
+ unordered: boolean;
42
+ }
43
+
44
+ /**
45
+ * Encode a DATA_CHANNEL_OPEN message.
46
+ * @param {Object} p
47
+ * @param {number} p.channelType
48
+ * @param {number} p.priority
49
+ * @param {number} p.reliabilityParameter
50
+ * @param {string} p.label
51
+ * @param {string} p.protocol
52
+ * @returns {Buffer}
53
+ */
54
+ export function encodeOpen({ channelType, priority = 0, reliabilityParameter = 0, label = '', protocol = '' }: OpenParams): Buffer {
55
+ const labelBuf = Buffer.from(label, 'utf8');
56
+ const protoBuf = Buffer.from(protocol, 'utf8');
57
+ const buf = Buffer.alloc(12 + labelBuf.length + protoBuf.length);
58
+ buf.writeUInt8(MESSAGE_TYPE.DATA_CHANNEL_OPEN, 0);
59
+ buf.writeUInt8(channelType, 1);
60
+ buf.writeUInt16BE(priority, 2);
61
+ buf.writeUInt32BE(reliabilityParameter >>> 0, 4);
62
+ buf.writeUInt16BE(labelBuf.length, 8);
63
+ buf.writeUInt16BE(protoBuf.length, 10);
64
+ labelBuf.copy(buf, 12);
65
+ protoBuf.copy(buf, 12 + labelBuf.length);
66
+ return buf;
67
+ }
68
+
69
+ /**
70
+ * Decode a DATA_CHANNEL_OPEN message.
71
+ * @param {Buffer} buf
72
+ * @returns {Object}
73
+ */
74
+ export function decodeOpen(buf: Buffer): DecodedOpen {
75
+ const channelType = buf.readUInt8(1);
76
+ const priority = buf.readUInt16BE(2);
77
+ const reliabilityParameter = buf.readUInt32BE(4);
78
+ const labelLen = buf.readUInt16BE(8);
79
+ const protoLen = buf.readUInt16BE(10);
80
+ const label = buf.slice(12, 12 + labelLen).toString('utf8');
81
+ const protocol = buf.slice(12 + labelLen, 12 + labelLen + protoLen).toString('utf8');
82
+ const unordered = (channelType & 0x80) !== 0;
83
+ return { channelType, priority, reliabilityParameter, label, protocol, unordered };
84
+ }
85
+
86
+ /** Encode a DATA_CHANNEL_ACK message. */
87
+ export function encodeAck(): Buffer {
88
+ return Buffer.from([MESSAGE_TYPE.DATA_CHANNEL_ACK]);
89
+ }
90
+
91
+ /** Return the message type of a DCEP buffer. */
92
+ export function messageType(buf: Buffer): number {
93
+ return buf.length > 0 ? buf.readUInt8(0) : -1;
94
+ }