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.
- package/README.md +94 -85
- package/dist/index.cjs +20 -5606
- package/dist/index.mjs +25 -5598
- package/dist/types/crypto/der.d.ts +107 -0
- package/dist/types/crypto/x509.d.ts +56 -0
- package/dist/types/datachannel/RTCDataChannel.d.ts +179 -0
- package/dist/types/dtls/RTCCertificate.d.ts +163 -0
- package/dist/types/dtls/cipher.d.ts +81 -0
- package/dist/types/dtls/connection.d.ts +81 -0
- package/dist/types/dtls/prf.d.ts +29 -0
- package/dist/types/dtls/protocol.d.ts +127 -0
- package/dist/types/foundation/ByteBufferQueue.d.ts +71 -0
- package/dist/types/foundation/RTCError.d.ts +152 -0
- package/dist/types/ice/RTCIceCandidate.d.ts +161 -0
- package/dist/types/ice/ice-agent.d.ts +154 -0
- package/dist/types/ice/stun-message.d.ts +92 -0
- package/dist/types/index.d.ts +29 -0
- package/dist/types/peerconnection/RTCPeerConnection.d.ts +74 -0
- package/dist/types/sctp/association.d.ts +77 -0
- package/dist/types/sctp/chunks.d.ts +200 -0
- package/dist/types/sctp/crc32c.d.ts +24 -0
- package/dist/types/sctp/datachannel-manager.d.ts +51 -0
- package/dist/types/sctp/dcep.d.ts +56 -0
- package/dist/types/sdp/RTCSessionDescription.d.ts +73 -0
- package/dist/types/sdp/sdp-utils.d.ts +103 -0
- package/dist/types/stun/stun-client.d.ts +119 -0
- package/dist/types/transport-stack.d.ts +68 -0
- package/package.json +26 -21
- package/src/crypto/der.ts +205 -0
- package/src/crypto/x509.ts +146 -0
- package/src/datachannel/RTCDataChannel.ts +388 -0
- package/src/dtls/RTCCertificate.ts +396 -0
- package/src/dtls/cipher.ts +198 -0
- package/src/dtls/connection.ts +974 -0
- package/src/dtls/prf.ts +62 -0
- package/src/dtls/protocol.ts +204 -0
- package/src/foundation/{ByteBufferQueue.js → ByteBufferQueue.ts} +74 -72
- package/src/foundation/{RTCError.js → RTCError.ts} +110 -60
- package/src/ice/{RTCIceCandidate.js → RTCIceCandidate.ts} +140 -92
- package/src/ice/ice-agent.ts +609 -0
- package/src/ice/stun-message.ts +260 -0
- package/src/index.ts +72 -0
- package/src/peerconnection/RTCPeerConnection.ts +430 -0
- package/src/sctp/association.ts +523 -0
- package/src/sctp/chunks.ts +350 -0
- package/src/sctp/crc32c.ts +57 -0
- package/src/sctp/datachannel-manager.ts +187 -0
- package/src/sctp/dcep.ts +94 -0
- package/src/sdp/{RTCSessionDescription.js → RTCSessionDescription.ts} +42 -29
- package/src/sdp/sdp-utils.ts +229 -0
- package/src/stun/{stun-client.js → stun-client.ts} +346 -187
- package/src/transport-stack.ts +165 -0
- package/dist/index.cjs.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/src/datachannel/RTCDataChannel.js +0 -354
- package/src/dtls/RTCCertificate.js +0 -310
- package/src/dtls/RTCDtlsTransport.js +0 -247
- package/src/ice/RTCIceTransport.js +0 -1018
- package/src/index.d.ts +0 -400
- package/src/index.js +0 -92
- package/src/network/network-transport.js +0 -478
- package/src/peerconnection/RTCPeerConnection.js +0 -875
- package/src/sctp/RTCSctpTransport.js +0 -253
- package/src/sdp/sdp-utils.js +0 -224
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file stun-message.ts
|
|
3
|
+
* @description STUN message codec for ICE connectivity checks (RFC 5389 / 8445).
|
|
4
|
+
* @module ice/stun-message
|
|
5
|
+
*
|
|
6
|
+
* Unlike the server-oriented stun-client.js (binding/allocate), this builds and
|
|
7
|
+
* validates the connectivity-check messages browsers require: USERNAME,
|
|
8
|
+
* MESSAGE-INTEGRITY (HMAC-SHA1 keyed by the peer's ice-pwd), FINGERPRINT
|
|
9
|
+
* (CRC-32 of the message Xored with 0x5354554e), PRIORITY, ICE-CONTROLLING/
|
|
10
|
+
* ICE-CONTROLLED, and USE-CANDIDATE.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
import * as crypto from 'crypto';
|
|
16
|
+
|
|
17
|
+
export const MAGIC_COOKIE = 0x2112a442;
|
|
18
|
+
|
|
19
|
+
export const METHOD = Object.freeze({ BINDING: 0x0001 });
|
|
20
|
+
export const CLASS = Object.freeze({
|
|
21
|
+
REQUEST: 0x000,
|
|
22
|
+
INDICATION: 0x010,
|
|
23
|
+
SUCCESS: 0x100,
|
|
24
|
+
ERROR: 0x110,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export const MSG_TYPE = Object.freeze({
|
|
28
|
+
BINDING_REQUEST: 0x0001,
|
|
29
|
+
BINDING_SUCCESS: 0x0101,
|
|
30
|
+
BINDING_ERROR: 0x0111,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export const ATTR = Object.freeze({
|
|
34
|
+
MAPPED_ADDRESS: 0x0001,
|
|
35
|
+
USERNAME: 0x0006,
|
|
36
|
+
MESSAGE_INTEGRITY: 0x0008,
|
|
37
|
+
ERROR_CODE: 0x0009,
|
|
38
|
+
XOR_MAPPED_ADDRESS: 0x0020,
|
|
39
|
+
PRIORITY: 0x0024,
|
|
40
|
+
USE_CANDIDATE: 0x0025,
|
|
41
|
+
FINGERPRINT: 0x8028,
|
|
42
|
+
ICE_CONTROLLED: 0x8029,
|
|
43
|
+
ICE_CONTROLLING: 0x802a,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/** A single STUN attribute pending serialization. */
|
|
47
|
+
interface StunAttribute {
|
|
48
|
+
type: number;
|
|
49
|
+
value: Buffer;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Shape of a parsed STUN message. */
|
|
53
|
+
export interface ParsedStunMessage {
|
|
54
|
+
type: number;
|
|
55
|
+
transactionId: Buffer;
|
|
56
|
+
attrs: Map<number, Buffer>;
|
|
57
|
+
raw: Buffer;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function pad4(n: number): number {
|
|
61
|
+
return (n + 3) & ~3;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** CRC-32 (IEEE) for the FINGERPRINT attribute. */
|
|
65
|
+
const CRC_TABLE: Uint32Array = (() => {
|
|
66
|
+
const t = new Uint32Array(256);
|
|
67
|
+
for (let n = 0; n < 256; n++) {
|
|
68
|
+
let c = n;
|
|
69
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
70
|
+
t[n] = c >>> 0;
|
|
71
|
+
}
|
|
72
|
+
return t;
|
|
73
|
+
})();
|
|
74
|
+
export function crc32(buf: Buffer): number {
|
|
75
|
+
let crc = 0xffffffff;
|
|
76
|
+
for (let i = 0; i < buf.length; i++) crc = CRC_TABLE[(crc ^ buf[i]!) & 0xff]! ^ (crc >>> 8);
|
|
77
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @class StunMessageBuilder
|
|
82
|
+
* @description Incrementally builds a STUN message, then appends
|
|
83
|
+
* MESSAGE-INTEGRITY and FINGERPRINT with the correct length pre-computation.
|
|
84
|
+
*/
|
|
85
|
+
export class StunMessageBuilder {
|
|
86
|
+
type: number;
|
|
87
|
+
transactionId: Buffer;
|
|
88
|
+
attrs: StunAttribute[];
|
|
89
|
+
|
|
90
|
+
constructor(type: number, transactionId?: Buffer) {
|
|
91
|
+
this.type = type;
|
|
92
|
+
this.transactionId = transactionId || crypto.randomBytes(12);
|
|
93
|
+
this.attrs = []; // {type, value}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
addAttr(type: number, value: Buffer): this {
|
|
97
|
+
this.attrs.push({ type, value });
|
|
98
|
+
return this;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
addUsername(username: string): this {
|
|
102
|
+
return this.addAttr(ATTR.USERNAME, Buffer.from(username, 'utf8'));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
addPriority(priority: number): this {
|
|
106
|
+
const b = Buffer.alloc(4);
|
|
107
|
+
b.writeUInt32BE(priority >>> 0, 0);
|
|
108
|
+
return this.addAttr(ATTR.PRIORITY, b);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
addIceControlling(tieBreaker: Buffer): this {
|
|
112
|
+
return this.addAttr(ATTR.ICE_CONTROLLING, tieBreaker);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
addIceControlled(tieBreaker: Buffer): this {
|
|
116
|
+
return this.addAttr(ATTR.ICE_CONTROLLED, tieBreaker);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
addUseCandidate(): this {
|
|
120
|
+
return this.addAttr(ATTR.USE_CANDIDATE, Buffer.alloc(0));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
addXorMappedAddress(address: string, port: number): this {
|
|
124
|
+
return this.addAttr(ATTR.XOR_MAPPED_ADDRESS, encodeXorAddress(address, port, this.transactionId));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Serialize the attributes added so far. */
|
|
128
|
+
#encodeBody(): Buffer {
|
|
129
|
+
const parts: Buffer[] = [];
|
|
130
|
+
for (const a of this.attrs) {
|
|
131
|
+
const head = Buffer.alloc(4);
|
|
132
|
+
head.writeUInt16BE(a.type, 0);
|
|
133
|
+
head.writeUInt16BE(a.value.length, 2);
|
|
134
|
+
const padded = Buffer.alloc(pad4(a.value.length));
|
|
135
|
+
a.value.copy(padded, 0);
|
|
136
|
+
parts.push(head, padded);
|
|
137
|
+
}
|
|
138
|
+
return Buffer.concat(parts);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#header(bodyLen: number): Buffer {
|
|
142
|
+
const h = Buffer.alloc(20);
|
|
143
|
+
h.writeUInt16BE(this.type, 0);
|
|
144
|
+
h.writeUInt16BE(bodyLen, 2);
|
|
145
|
+
h.writeUInt32BE(MAGIC_COOKIE, 4);
|
|
146
|
+
this.transactionId.copy(h, 8);
|
|
147
|
+
return h;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Finalize the message, appending MESSAGE-INTEGRITY (keyed by `password`)
|
|
152
|
+
* and FINGERPRINT. Both require the header length to include the attribute
|
|
153
|
+
* being computed, per RFC 5389 §15.4 / §15.5.
|
|
154
|
+
* @param {string} [password] - ICE password for MESSAGE-INTEGRITY
|
|
155
|
+
* @returns {Buffer}
|
|
156
|
+
*/
|
|
157
|
+
build(password?: string): Buffer {
|
|
158
|
+
let body = this.#encodeBody();
|
|
159
|
+
|
|
160
|
+
if (password) {
|
|
161
|
+
// Length for HMAC input = current body + (4 header + 20 HMAC).
|
|
162
|
+
const lenForMI = body.length + 24;
|
|
163
|
+
const header = this.#header(lenForMI);
|
|
164
|
+
const hmac = crypto
|
|
165
|
+
.createHmac('sha1', Buffer.from(password, 'utf8'))
|
|
166
|
+
.update(Buffer.concat([header, body]))
|
|
167
|
+
.digest();
|
|
168
|
+
const miHead = Buffer.alloc(4);
|
|
169
|
+
miHead.writeUInt16BE(ATTR.MESSAGE_INTEGRITY, 0);
|
|
170
|
+
miHead.writeUInt16BE(20, 2);
|
|
171
|
+
body = Buffer.concat([body, miHead, hmac]);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// FINGERPRINT: CRC-32 over the message (with length including fingerprint)
|
|
175
|
+
// Xored with 0x5354554e.
|
|
176
|
+
const lenForFp = body.length + 8;
|
|
177
|
+
const headerFp = this.#header(lenForFp);
|
|
178
|
+
const fpVal = (crc32(Buffer.concat([headerFp, body])) ^ 0x5354554e) >>> 0;
|
|
179
|
+
const fpHead = Buffer.alloc(8);
|
|
180
|
+
fpHead.writeUInt16BE(ATTR.FINGERPRINT, 0);
|
|
181
|
+
fpHead.writeUInt16BE(4, 2);
|
|
182
|
+
fpHead.writeUInt32BE(fpVal, 4);
|
|
183
|
+
body = Buffer.concat([body, fpHead]);
|
|
184
|
+
|
|
185
|
+
return Buffer.concat([this.#header(body.length), body]);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Encode a XOR-MAPPED-ADDRESS attribute value (IPv4). */
|
|
190
|
+
function encodeXorAddress(address: string, port: number, _transactionId: Buffer): Buffer {
|
|
191
|
+
const buf = Buffer.alloc(8);
|
|
192
|
+
buf.writeUInt8(0, 0);
|
|
193
|
+
buf.writeUInt8(0x01, 1); // family IPv4
|
|
194
|
+
buf.writeUInt16BE(port ^ (MAGIC_COOKIE >>> 16), 2);
|
|
195
|
+
const parts = address.split('.').map(Number);
|
|
196
|
+
const addrInt = ((parts[0]! << 24) | (parts[1]! << 16) | (parts[2]! << 8) | parts[3]!) >>> 0;
|
|
197
|
+
buf.writeUInt32BE((addrInt ^ MAGIC_COOKIE) >>> 0, 4);
|
|
198
|
+
return buf;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Parse a STUN message. Returns null if not a STUN message.
|
|
203
|
+
* @param {Buffer} msg
|
|
204
|
+
* @returns {null|{type:number,transactionId:Buffer,attrs:Map<number,Buffer>,raw:Buffer}}
|
|
205
|
+
*/
|
|
206
|
+
export function parse(msg: Buffer): ParsedStunMessage | null {
|
|
207
|
+
if (msg.length < 20) return null;
|
|
208
|
+
if (msg.readUInt32BE(4) !== MAGIC_COOKIE) return null;
|
|
209
|
+
const type = msg.readUInt16BE(0);
|
|
210
|
+
const length = msg.readUInt16BE(2);
|
|
211
|
+
if (20 + length > msg.length) return null;
|
|
212
|
+
const transactionId = msg.slice(8, 20);
|
|
213
|
+
const attrs = new Map<number, Buffer>();
|
|
214
|
+
let off = 20;
|
|
215
|
+
const end = 20 + length;
|
|
216
|
+
while (off + 4 <= end) {
|
|
217
|
+
const atype = msg.readUInt16BE(off);
|
|
218
|
+
const alen = msg.readUInt16BE(off + 2);
|
|
219
|
+
off += 4;
|
|
220
|
+
if (off + alen > end) break;
|
|
221
|
+
attrs.set(atype, msg.slice(off, off + alen));
|
|
222
|
+
off += pad4(alen);
|
|
223
|
+
}
|
|
224
|
+
return { type, transactionId, attrs, raw: msg };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Verify the MESSAGE-INTEGRITY of a parsed message against a password.
|
|
229
|
+
* @param {Buffer} msg - raw message
|
|
230
|
+
* @param {string} password
|
|
231
|
+
* @returns {boolean}
|
|
232
|
+
*/
|
|
233
|
+
export function verifyIntegrity(msg: Buffer, password: string): boolean {
|
|
234
|
+
// Locate the MESSAGE-INTEGRITY attribute.
|
|
235
|
+
const length = msg.readUInt16BE(2);
|
|
236
|
+
let off = 20;
|
|
237
|
+
const end = 20 + length;
|
|
238
|
+
let miOffset = -1;
|
|
239
|
+
while (off + 4 <= end) {
|
|
240
|
+
const atype = msg.readUInt16BE(off);
|
|
241
|
+
const alen = msg.readUInt16BE(off + 2);
|
|
242
|
+
if (atype === ATTR.MESSAGE_INTEGRITY) {
|
|
243
|
+
miOffset = off;
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
off += 4 + pad4(alen);
|
|
247
|
+
}
|
|
248
|
+
if (miOffset < 0) return false;
|
|
249
|
+
|
|
250
|
+
const provided = msg.slice(miOffset + 4, miOffset + 4 + 20);
|
|
251
|
+
// Recompute over header (with length up to & including MI) + body before MI.
|
|
252
|
+
const lenUpToMI = miOffset + 24 - 20;
|
|
253
|
+
const header = Buffer.from(msg.slice(0, 20));
|
|
254
|
+
header.writeUInt16BE(lenUpToMI, 2);
|
|
255
|
+
const hmac = crypto
|
|
256
|
+
.createHmac('sha1', Buffer.from(password, 'utf8'))
|
|
257
|
+
.update(Buffer.concat([header, msg.slice(20, miOffset)]))
|
|
258
|
+
.digest();
|
|
259
|
+
return provided.length === hmac.length && crypto.timingSafeEqual(provided, hmac);
|
|
260
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview node-rtc-connection - WebRTC DataChannel implementation for Node.js
|
|
3
|
+
*
|
|
4
|
+
* A from-scratch, pure-Node.js implementation of WebRTC peer connections and
|
|
5
|
+
* data channels (no native dependencies). Interoperates with browsers.
|
|
6
|
+
*
|
|
7
|
+
* This implementation focuses on DataChannel functionality without media streams.
|
|
8
|
+
* Features:
|
|
9
|
+
* - ICE (Interactive Connectivity Establishment), RFC 8445
|
|
10
|
+
* - STUN/TURN support for NAT traversal
|
|
11
|
+
* - DTLS 1.2 encryption (RFC 6347)
|
|
12
|
+
* - SCTP over DTLS + DCEP for data channels (RFC 8831 / 8832)
|
|
13
|
+
* - W3C-compatible RTCPeerConnection / RTCDataChannel API
|
|
14
|
+
*
|
|
15
|
+
* @license MIT
|
|
16
|
+
* @author nmhung1210
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// Foundation
|
|
20
|
+
import ByteBufferQueue from './foundation/ByteBufferQueue';
|
|
21
|
+
import RTCError from './foundation/RTCError';
|
|
22
|
+
|
|
23
|
+
// ICE / certificates
|
|
24
|
+
import RTCIceCandidate from './ice/RTCIceCandidate';
|
|
25
|
+
import RTCCertificate from './dtls/RTCCertificate';
|
|
26
|
+
|
|
27
|
+
// DataChannel
|
|
28
|
+
import { RTCDataChannel, RTCDataChannelState } from './datachannel/RTCDataChannel';
|
|
29
|
+
|
|
30
|
+
// SDP
|
|
31
|
+
import { RTCSessionDescription, RTCSdpType } from './sdp/RTCSessionDescription';
|
|
32
|
+
|
|
33
|
+
// PeerConnection
|
|
34
|
+
import {
|
|
35
|
+
RTCPeerConnection,
|
|
36
|
+
RTCSignalingState,
|
|
37
|
+
RTCIceGatheringState,
|
|
38
|
+
RTCPeerConnectionState,
|
|
39
|
+
} from './peerconnection/RTCPeerConnection';
|
|
40
|
+
|
|
41
|
+
import pkg from '../package.json';
|
|
42
|
+
|
|
43
|
+
export {
|
|
44
|
+
// Foundation
|
|
45
|
+
ByteBufferQueue,
|
|
46
|
+
RTCError,
|
|
47
|
+
|
|
48
|
+
// ICE / certificates
|
|
49
|
+
RTCIceCandidate,
|
|
50
|
+
RTCCertificate,
|
|
51
|
+
|
|
52
|
+
// DataChannel
|
|
53
|
+
RTCDataChannel,
|
|
54
|
+
RTCDataChannelState,
|
|
55
|
+
|
|
56
|
+
// SDP
|
|
57
|
+
RTCSessionDescription,
|
|
58
|
+
RTCSdpType,
|
|
59
|
+
|
|
60
|
+
// PeerConnection
|
|
61
|
+
RTCPeerConnection,
|
|
62
|
+
RTCSignalingState,
|
|
63
|
+
RTCIceGatheringState,
|
|
64
|
+
RTCPeerConnectionState,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const version: string = pkg.version;
|
|
68
|
+
|
|
69
|
+
// Re-export public types for consumers.
|
|
70
|
+
export type { RTCDataChannelInit } from './datachannel/RTCDataChannel';
|
|
71
|
+
export type { RTCSessionDescriptionInit } from './sdp/RTCSessionDescription';
|
|
72
|
+
export type { TransportStackOptions } from './transport-stack';
|