@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/dist/association.d.ts +167 -0
- package/dist/association.d.ts.map +1 -0
- package/dist/association.js +1394 -0
- package/dist/association.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/packet.d.ts +45 -0
- package/dist/packet.d.ts.map +1 -0
- package/dist/packet.js +156 -0
- package/dist/packet.js.map +1 -0
- package/dist/types.d.ts +62 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +56 -0
- package/src/association.ts +1621 -0
- package/src/index.ts +3 -0
- package/src/packet.ts +227 -0
- package/src/types.ts +76 -0
package/src/index.ts
ADDED
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
|
+
}
|