@agentdance/node-webrtc-sctp 1.0.0

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.
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './types.js';
2
+ export * from './packet.js';
3
+ export * from './association.js';
package/src/packet.ts ADDED
@@ -0,0 +1,227 @@
1
+ // SCTP packet codec – RFC 4960
2
+ // Encode/decode SCTP packets (common header + chunks)
3
+
4
+ import type { ChunkType } from './types.js';
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // SCTP Common Header (RFC 4960 §3.1)
8
+ // Source Port (16) | Dest Port (16) | Verification Tag (32) | Checksum (32)
9
+ // ---------------------------------------------------------------------------
10
+
11
+ export interface SctpCommonHeader {
12
+ srcPort: number;
13
+ dstPort: number;
14
+ verificationTag: number;
15
+ checksum: number;
16
+ }
17
+
18
+ export interface SctpChunk {
19
+ type: number; // ChunkType
20
+ flags: number;
21
+ value: Buffer;
22
+ }
23
+
24
+ export interface SctpPacket {
25
+ header: SctpCommonHeader;
26
+ chunks: SctpChunk[];
27
+ }
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Adler-32 / CRC-32c checksum for SCTP
31
+ // RFC 4960 uses CRC-32c. We compute it with a pure-JS implementation.
32
+ // ---------------------------------------------------------------------------
33
+
34
+ // CRC-32c table
35
+ const CRC32C_TABLE: Uint32Array = (() => {
36
+ const table = new Uint32Array(256);
37
+ for (let i = 0; i < 256; i++) {
38
+ let crc = i;
39
+ for (let j = 0; j < 8; j++) {
40
+ crc = (crc & 1) ? (0x82f63b78 ^ (crc >>> 1)) : (crc >>> 1);
41
+ }
42
+ table[i] = crc >>> 0;
43
+ }
44
+ return table;
45
+ })();
46
+
47
+ export function crc32c(buf: Buffer): number {
48
+ let crc = 0xffffffff;
49
+ for (let i = 0; i < buf.length; i++) {
50
+ crc = CRC32C_TABLE[(crc ^ buf[i]!) & 0xff]! ^ (crc >>> 8);
51
+ }
52
+ return (crc ^ 0xffffffff) >>> 0;
53
+ }
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Encode SCTP packet
57
+ // ---------------------------------------------------------------------------
58
+
59
+ export function encodeSctpPacket(packet: SctpPacket): Buffer {
60
+ // Encode chunks first
61
+ const chunkBuffers: Buffer[] = [];
62
+ for (const chunk of packet.chunks) {
63
+ const chunkLen = 4 + chunk.value.length;
64
+ const padded = Math.ceil(chunkLen / 4) * 4;
65
+ const buf = Buffer.alloc(padded, 0);
66
+ buf[0] = chunk.type;
67
+ buf[1] = chunk.flags;
68
+ buf.writeUInt16BE(chunkLen, 2);
69
+ chunk.value.copy(buf, 4);
70
+ chunkBuffers.push(buf);
71
+ }
72
+
73
+ const chunksTotal = chunkBuffers.reduce((s, b) => s + b.length, 0);
74
+ const packet_buf = Buffer.alloc(12 + chunksTotal);
75
+
76
+ // Write common header (checksum = 0 initially)
77
+ packet_buf.writeUInt16BE(packet.header.srcPort, 0);
78
+ packet_buf.writeUInt16BE(packet.header.dstPort, 2);
79
+ packet_buf.writeUInt32BE(packet.header.verificationTag, 4);
80
+ packet_buf.writeUInt32BE(0, 8); // checksum placeholder
81
+
82
+ let off = 12;
83
+ for (const cb of chunkBuffers) {
84
+ cb.copy(packet_buf, off);
85
+ off += cb.length;
86
+ }
87
+
88
+ // Compute and write checksum in little-endian (usrsctp/libwebrtc convention)
89
+ // RFC 4960 says network byte order, but all real SCTP stacks (Linux, usrsctp)
90
+ // write CRC-32c in little-endian due to the reflected/LSB-first nature of the algorithm.
91
+ const checksum = crc32c(packet_buf);
92
+ packet_buf.writeUInt32LE(checksum, 8);
93
+
94
+ return packet_buf;
95
+ }
96
+
97
+ // ---------------------------------------------------------------------------
98
+ // Decode SCTP packet
99
+ // ---------------------------------------------------------------------------
100
+
101
+ export function decodeSctpPacket(buf: Buffer): SctpPacket {
102
+ if (buf.length < 12) throw new RangeError('SCTP packet too short');
103
+
104
+ const header: SctpCommonHeader = {
105
+ srcPort: buf.readUInt16BE(0),
106
+ dstPort: buf.readUInt16BE(2),
107
+ verificationTag: buf.readUInt32BE(4),
108
+ checksum: buf.readUInt32BE(8),
109
+ };
110
+
111
+ const chunks: SctpChunk[] = [];
112
+ let off = 12;
113
+ while (off + 4 <= buf.length) {
114
+ const type = buf[off]!;
115
+ const flags = buf[off + 1]!;
116
+ const length = buf.readUInt16BE(off + 2);
117
+ if (length < 4 || off + length > buf.length) break;
118
+ const value = buf.subarray(off + 4, off + length);
119
+ chunks.push({ type, flags, value: Buffer.from(value) });
120
+ // Skip to next chunk (4-byte aligned)
121
+ off += Math.ceil(length / 4) * 4;
122
+ }
123
+
124
+ return { header, chunks };
125
+ }
126
+
127
+ // ---------------------------------------------------------------------------
128
+ // Encode DATA chunk value (RFC 4960 §3.3.1)
129
+ // TSN (32) | SID (16) | SSN (16) | PPID (32) | payload
130
+ // ---------------------------------------------------------------------------
131
+
132
+ export interface SctpDataPayload {
133
+ tsn: number;
134
+ streamId: number;
135
+ ssn: number; // Stream Sequence Number
136
+ ppid: number;
137
+ userData: Buffer;
138
+ beginning: boolean; // B flag
139
+ ending: boolean; // E flag
140
+ unordered: boolean; // U flag
141
+ }
142
+
143
+ export function encodeDataChunk(data: SctpDataPayload): SctpChunk {
144
+ const value = Buffer.allocUnsafe(12 + data.userData.length);
145
+ value.writeUInt32BE(data.tsn, 0);
146
+ value.writeUInt16BE(data.streamId, 4);
147
+ value.writeUInt16BE(data.ssn, 6);
148
+ value.writeUInt32BE(data.ppid, 8);
149
+ data.userData.copy(value, 12);
150
+
151
+ let flags = 0;
152
+ if (data.unordered) flags |= 0x04;
153
+ if (data.beginning) flags |= 0x02;
154
+ if (data.ending) flags |= 0x01;
155
+
156
+ return { type: 0 /* DATA */, flags, value };
157
+ }
158
+
159
+ export function decodeDataChunk(chunk: SctpChunk): SctpDataPayload {
160
+ if (chunk.value.length < 12) throw new RangeError('DATA chunk too short');
161
+ return {
162
+ tsn: chunk.value.readUInt32BE(0),
163
+ streamId: chunk.value.readUInt16BE(4),
164
+ ssn: chunk.value.readUInt16BE(6),
165
+ ppid: chunk.value.readUInt32BE(8),
166
+ userData: Buffer.from(chunk.value.subarray(12)),
167
+ beginning: !!(chunk.flags & 0x02),
168
+ ending: !!(chunk.flags & 0x01),
169
+ unordered: !!(chunk.flags & 0x04),
170
+ };
171
+ }
172
+
173
+ // ---------------------------------------------------------------------------
174
+ // DCEP message encode/decode (RFC 8832)
175
+ // ---------------------------------------------------------------------------
176
+
177
+ export interface DcepOpen {
178
+ type: 0x03; // DATA_CHANNEL_OPEN
179
+ channelType: number;
180
+ priority: number;
181
+ reliabilityParam: number;
182
+ label: string;
183
+ protocol: string;
184
+ }
185
+
186
+ export interface DcepAck {
187
+ type: 0x02; // DATA_CHANNEL_ACK
188
+ }
189
+
190
+ export function encodeDcepOpen(msg: DcepOpen): Buffer {
191
+ const labelBuf = Buffer.from(msg.label, 'utf8');
192
+ const protoBuf = Buffer.from(msg.protocol, 'utf8');
193
+ const buf = Buffer.allocUnsafe(12 + labelBuf.length + protoBuf.length);
194
+ buf[0] = 0x03; // DATA_CHANNEL_OPEN
195
+ buf[1] = msg.channelType;
196
+ buf.writeUInt16BE(msg.priority, 2);
197
+ buf.writeUInt32BE(msg.reliabilityParam, 4);
198
+ buf.writeUInt16BE(labelBuf.length, 8);
199
+ buf.writeUInt16BE(protoBuf.length, 10);
200
+ labelBuf.copy(buf, 12);
201
+ protoBuf.copy(buf, 12 + labelBuf.length);
202
+ return buf;
203
+ }
204
+
205
+ export function encodeDcepAck(): Buffer {
206
+ return Buffer.from([0x02, 0x00, 0x00, 0x00]);
207
+ }
208
+
209
+ export function decodeDcep(buf: Buffer): DcepOpen | DcepAck {
210
+ if (buf.length < 1) throw new RangeError('DCEP message too short');
211
+ const msgType = buf[0]!;
212
+ if (msgType === 0x02) {
213
+ return { type: 0x02 };
214
+ }
215
+ if (msgType === 0x03) {
216
+ if (buf.length < 12) throw new RangeError('DCEP OPEN too short');
217
+ const channelType = buf[1]!;
218
+ const priority = buf.readUInt16BE(2);
219
+ const reliabilityParam = buf.readUInt32BE(4);
220
+ const labelLen = buf.readUInt16BE(8);
221
+ const protoLen = buf.readUInt16BE(10);
222
+ const label = buf.subarray(12, 12 + labelLen).toString('utf8');
223
+ const protocol = buf.subarray(12 + labelLen, 12 + labelLen + protoLen).toString('utf8');
224
+ return { type: 0x03, channelType, priority, reliabilityParam, label, protocol };
225
+ }
226
+ throw new Error(`Unknown DCEP type: 0x${msgType.toString(16)}`);
227
+ }
package/src/types.ts ADDED
@@ -0,0 +1,76 @@
1
+ // SCTP over DTLS – RFC 4960 / RFC 8832 (DCEP)
2
+ // ---------------------------------------------------------------------------
3
+ // Types for SCTP and Data Channels
4
+
5
+ export type SctpState = 'new' | 'connecting' | 'connected' | 'closed' | 'failed';
6
+ export type DataChannelState = 'connecting' | 'open' | 'closing' | 'closed';
7
+ export type DataChannelType = 'reliable' | 'reliable-ordered' | 'partial-reliable-rexmit' | 'partial-reliable-timed';
8
+
9
+ export interface SctpParameters {
10
+ port: number; // SCTP port (5000 by default)
11
+ maxMessageSize: number;
12
+ }
13
+
14
+ export interface DataChannelOptions {
15
+ label: string;
16
+ ordered?: boolean;
17
+ maxPacketLifeTime?: number; // ms
18
+ maxRetransmits?: number;
19
+ protocol?: string;
20
+ negotiated?: boolean;
21
+ id?: number;
22
+ }
23
+
24
+ export interface DataChannelInfo {
25
+ id: number;
26
+ label: string;
27
+ protocol: string;
28
+ ordered: boolean;
29
+ maxPacketLifeTime: number | undefined;
30
+ maxRetransmits: number | undefined;
31
+ state: DataChannelState;
32
+ negotiated?: boolean;
33
+ }
34
+
35
+ // SCTP chunk types (RFC 4960)
36
+ export const enum ChunkType {
37
+ DATA = 0,
38
+ INIT = 1,
39
+ INIT_ACK = 2,
40
+ SACK = 3,
41
+ HEARTBEAT = 4,
42
+ HEARTBEAT_ACK = 5,
43
+ ABORT = 6,
44
+ SHUTDOWN = 7,
45
+ SHUTDOWN_ACK = 8,
46
+ ERROR = 9,
47
+ COOKIE_ECHO = 10,
48
+ COOKIE_ACK = 11,
49
+ SHUTDOWN_COMPLETE = 14,
50
+ FORWARD_TSN = 192,
51
+ }
52
+
53
+ // PPID values (RFC 8832)
54
+ export const enum Ppid {
55
+ DCEP = 50, // DataChannel Establish Protocol
56
+ STRING = 51, // UTF-8 string
57
+ BINARY = 53, // Binary data
58
+ STRING_EMPTY = 56, // Empty string
59
+ BINARY_EMPTY = 57, // Empty binary
60
+ }
61
+
62
+ // DCEP message types (RFC 8832)
63
+ export const enum DcepType {
64
+ DATA_CHANNEL_OPEN = 0x03,
65
+ DATA_CHANNEL_ACK = 0x02,
66
+ }
67
+
68
+ // DataChannel open channel types
69
+ export const enum DcepChannelType {
70
+ RELIABLE = 0x00,
71
+ PARTIAL_RELIABLE_REXMIT = 0x01,
72
+ PARTIAL_RELIABLE_TIMED = 0x02,
73
+ RELIABLE_UNORDERED = 0x80,
74
+ PARTIAL_RELIABLE_REXMIT_UNORDERED = 0x81,
75
+ PARTIAL_RELIABLE_TIMED_UNORDERED = 0x82,
76
+ }