node-rtc-connection 1.0.12 → 1.0.15
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 +355 -289
- package/dist/index.cjs +4357 -3092
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +4357 -3092
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
- package/src/datachannel/RTCDataChannel.js +354 -0
- package/src/dtls/RTCCertificate.js +310 -0
- package/src/dtls/RTCDtlsTransport.js +247 -0
- package/src/foundation/ByteBufferQueue.js +235 -0
- package/src/foundation/RTCError.js +226 -0
- package/src/ice/RTCIceCandidate.js +301 -0
- package/src/ice/RTCIceTransport.js +998 -0
- package/src/index.d.ts +316 -145
- package/src/index.js +78 -45
- package/src/network/network-transport.js +478 -0
- package/src/peerconnection/RTCPeerConnection.js +847 -0
- package/src/sctp/RTCSctpTransport.js +253 -0
- package/src/sdp/RTCSessionDescription.js +102 -0
- package/src/sdp/sdp-utils.js +224 -0
- package/src/stun/stun-client.js +643 -0
- package/src/ICEGatherer.js +0 -341
- package/src/NativePeerConnectionFactory.js +0 -1044
- package/src/RTCDataChannel.js +0 -346
- package/src/RTCDataChannelEvent.js +0 -50
- package/src/RTCError.js +0 -66
- package/src/RTCIceCandidate.js +0 -184
- package/src/RTCPeerConnection.js +0 -505
- package/src/RTCPeerConnectionIceEvent.js +0 -58
- package/src/RTCSessionDescription.js +0 -62
- package/src/STUNClient.js +0 -222
- package/src/SecureConnection.js +0 -298
- package/src/TURNClient.js +0 -561
- package/src/UDPTransport.js +0 -236
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file network-transport.js
|
|
3
|
+
* @description Real UDP/TCP network transport implementation
|
|
4
|
+
* @module network/network-transport
|
|
5
|
+
*
|
|
6
|
+
* Provides real networking capabilities using Node.js net and dgram packages
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const dgram = require('dgram');
|
|
12
|
+
const net = require('net');
|
|
13
|
+
const EventEmitter = require('events');
|
|
14
|
+
const crypto = require('crypto');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Transport protocol types
|
|
18
|
+
*/
|
|
19
|
+
const TransportProtocol = Object.freeze({
|
|
20
|
+
UDP: 'udp',
|
|
21
|
+
TCP: 'tcp'
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @class NetworkTransport
|
|
26
|
+
* @extends EventEmitter
|
|
27
|
+
* @description Base class for network transport
|
|
28
|
+
*
|
|
29
|
+
* Events:
|
|
30
|
+
* - 'data': (data, rinfo) - Data received
|
|
31
|
+
* - 'error': (error) - Error occurred
|
|
32
|
+
* - 'listening': () - Socket is listening
|
|
33
|
+
* - 'close': () - Socket closed
|
|
34
|
+
*/
|
|
35
|
+
class NetworkTransport extends EventEmitter {
|
|
36
|
+
constructor(protocol = TransportProtocol.UDP) {
|
|
37
|
+
super();
|
|
38
|
+
this.protocol = protocol;
|
|
39
|
+
this.socket = null;
|
|
40
|
+
this.localAddress = null;
|
|
41
|
+
this.localPort = null;
|
|
42
|
+
this.isListening = false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Bind to a local address and port
|
|
47
|
+
* @param {number} [port=0] - Port to bind (0 for random)
|
|
48
|
+
* @param {string} [address='0.0.0.0'] - Address to bind
|
|
49
|
+
* @returns {Promise<{address: string, port: number}>}
|
|
50
|
+
*/
|
|
51
|
+
async bind(port = 0, address = '0.0.0.0') {
|
|
52
|
+
throw new Error('Must be implemented by subclass');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Send data to remote address
|
|
57
|
+
* @param {Buffer} data - Data to send
|
|
58
|
+
* @param {string} address - Remote address
|
|
59
|
+
* @param {number} port - Remote port
|
|
60
|
+
* @returns {Promise<void>}
|
|
61
|
+
*/
|
|
62
|
+
async send(data, address, port) {
|
|
63
|
+
throw new Error('Must be implemented by subclass');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Close the transport
|
|
68
|
+
*/
|
|
69
|
+
close() {
|
|
70
|
+
if (this.socket) {
|
|
71
|
+
this.socket.close();
|
|
72
|
+
this.socket = null;
|
|
73
|
+
}
|
|
74
|
+
this.isListening = false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @class UDPTransport
|
|
80
|
+
* @extends NetworkTransport
|
|
81
|
+
* @description UDP transport implementation using dgram
|
|
82
|
+
*/
|
|
83
|
+
class UDPTransport extends NetworkTransport {
|
|
84
|
+
constructor() {
|
|
85
|
+
super(TransportProtocol.UDP);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Bind to a local address and port
|
|
90
|
+
* @param {number} [port=0] - Port to bind (0 for random)
|
|
91
|
+
* @param {string} [address='0.0.0.0'] - Address to bind
|
|
92
|
+
* @returns {Promise<{address: string, port: number}>}
|
|
93
|
+
*/
|
|
94
|
+
async bind(port = 0, address = '0.0.0.0') {
|
|
95
|
+
if (this.socket) {
|
|
96
|
+
throw new Error('Already bound');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return new Promise((resolve, reject) => {
|
|
100
|
+
this.socket = dgram.createSocket('udp4');
|
|
101
|
+
|
|
102
|
+
this.socket.on('message', (msg, rinfo) => {
|
|
103
|
+
this.emit('data', msg, rinfo);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
this.socket.on('error', (error) => {
|
|
107
|
+
this.emit('error', error);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
this.socket.on('close', () => {
|
|
111
|
+
this.isListening = false;
|
|
112
|
+
this.emit('close');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
this.socket.bind(port, address, () => {
|
|
116
|
+
const addr = this.socket.address();
|
|
117
|
+
this.localAddress = addr.address;
|
|
118
|
+
this.localPort = addr.port;
|
|
119
|
+
this.isListening = true;
|
|
120
|
+
this.emit('listening');
|
|
121
|
+
resolve({ address: addr.address, port: addr.port });
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Send data to remote address
|
|
128
|
+
* @param {Buffer} data - Data to send
|
|
129
|
+
* @param {string} address - Remote address
|
|
130
|
+
* @param {number} port - Remote port
|
|
131
|
+
* @returns {Promise<void>}
|
|
132
|
+
*/
|
|
133
|
+
async send(data, address, port) {
|
|
134
|
+
if (!this.socket) {
|
|
135
|
+
throw new Error('Socket not bound');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return new Promise((resolve, reject) => {
|
|
139
|
+
this.socket.send(data, port, address, (error) => {
|
|
140
|
+
if (error) {
|
|
141
|
+
reject(error);
|
|
142
|
+
} else {
|
|
143
|
+
resolve();
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* @class TCPTransport
|
|
152
|
+
* @extends NetworkTransport
|
|
153
|
+
* @description TCP transport implementation using net
|
|
154
|
+
* Supports both server (listening) and client (connecting) modes
|
|
155
|
+
*/
|
|
156
|
+
class TCPTransport extends NetworkTransport {
|
|
157
|
+
constructor() {
|
|
158
|
+
super(TransportProtocol.TCP);
|
|
159
|
+
this.server = null;
|
|
160
|
+
this.connections = new Map(); // connection id -> socket
|
|
161
|
+
this.mode = null; // 'server' or 'client'
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Start listening for connections (server mode)
|
|
166
|
+
* @param {number} [port=0] - Port to listen on (0 for random)
|
|
167
|
+
* @param {string} [address='0.0.0.0'] - Address to bind
|
|
168
|
+
* @returns {Promise<{address: string, port: number}>}
|
|
169
|
+
*/
|
|
170
|
+
async listen(port = 0, address = '0.0.0.0') {
|
|
171
|
+
if (this.server) {
|
|
172
|
+
throw new Error('Already listening');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
this.mode = 'server';
|
|
176
|
+
|
|
177
|
+
return new Promise((resolve, reject) => {
|
|
178
|
+
this.server = net.createServer((socket) => {
|
|
179
|
+
this._handleConnection(socket);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
this.server.on('error', (error) => {
|
|
183
|
+
this.emit('error', error);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
this.server.on('close', () => {
|
|
187
|
+
this.isListening = false;
|
|
188
|
+
this.emit('close');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
this.server.listen(port, address, () => {
|
|
192
|
+
const addr = this.server.address();
|
|
193
|
+
this.localAddress = addr.address;
|
|
194
|
+
this.localPort = addr.port;
|
|
195
|
+
this.isListening = true;
|
|
196
|
+
this.emit('listening');
|
|
197
|
+
resolve({ address: addr.address, port: addr.port });
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Connect to remote server (client mode)
|
|
204
|
+
* @param {string} address - Remote address
|
|
205
|
+
* @param {number} port - Remote port
|
|
206
|
+
* @returns {Promise<void>}
|
|
207
|
+
*/
|
|
208
|
+
async connect(address, port) {
|
|
209
|
+
if (this.mode === 'server') {
|
|
210
|
+
throw new Error('Cannot connect in server mode');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
this.mode = 'client';
|
|
214
|
+
|
|
215
|
+
return new Promise((resolve, reject) => {
|
|
216
|
+
const socket = net.connect(port, address, () => {
|
|
217
|
+
this._handleConnection(socket);
|
|
218
|
+
resolve();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
socket.on('error', (error) => {
|
|
222
|
+
reject(error);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Handle new connection
|
|
229
|
+
* @param {net.Socket} socket - TCP socket
|
|
230
|
+
* @private
|
|
231
|
+
*/
|
|
232
|
+
_handleConnection(socket) {
|
|
233
|
+
const connectionId = crypto.randomBytes(8).toString('hex');
|
|
234
|
+
this.connections.set(connectionId, socket);
|
|
235
|
+
|
|
236
|
+
const rinfo = {
|
|
237
|
+
address: socket.remoteAddress,
|
|
238
|
+
port: socket.remotePort,
|
|
239
|
+
connectionId
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// Handle incoming data
|
|
243
|
+
socket.on('data', (data) => {
|
|
244
|
+
this.emit('data', data, rinfo);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Handle socket close
|
|
248
|
+
socket.on('close', () => {
|
|
249
|
+
this.connections.delete(connectionId);
|
|
250
|
+
this.emit('connectionClosed', connectionId);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Handle errors
|
|
254
|
+
socket.on('error', (error) => {
|
|
255
|
+
this.emit('error', error);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Emit connection event
|
|
259
|
+
this.emit('connection', connectionId, rinfo);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Send data to specific connection or address
|
|
264
|
+
* @param {Buffer} data - Data to send
|
|
265
|
+
* @param {string} addressOrConnectionId - Remote address or connection ID
|
|
266
|
+
* @param {number} [port] - Remote port (not needed if using connection ID)
|
|
267
|
+
* @returns {Promise<void>}
|
|
268
|
+
*/
|
|
269
|
+
async send(data, addressOrConnectionId, port) {
|
|
270
|
+
// If in server mode or already have connection, use connection ID
|
|
271
|
+
if (this.connections.has(addressOrConnectionId)) {
|
|
272
|
+
const socket = this.connections.get(addressOrConnectionId);
|
|
273
|
+
return new Promise((resolve, reject) => {
|
|
274
|
+
socket.write(data, (error) => {
|
|
275
|
+
if (error) reject(error);
|
|
276
|
+
else resolve();
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Otherwise, send to all connections (broadcast)
|
|
282
|
+
if (this.connections.size > 0) {
|
|
283
|
+
const promises = [];
|
|
284
|
+
for (const socket of this.connections.values()) {
|
|
285
|
+
promises.push(
|
|
286
|
+
new Promise((resolve, reject) => {
|
|
287
|
+
socket.write(data, (error) => {
|
|
288
|
+
if (error) reject(error);
|
|
289
|
+
else resolve();
|
|
290
|
+
});
|
|
291
|
+
})
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
await Promise.all(promises);
|
|
295
|
+
} else {
|
|
296
|
+
throw new Error('No active connections');
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Close specific connection or all connections
|
|
302
|
+
* @param {string} [connectionId] - Connection ID to close (omit to close all)
|
|
303
|
+
*/
|
|
304
|
+
closeConnection(connectionId) {
|
|
305
|
+
if (connectionId) {
|
|
306
|
+
const socket = this.connections.get(connectionId);
|
|
307
|
+
if (socket) {
|
|
308
|
+
socket.end();
|
|
309
|
+
this.connections.delete(connectionId);
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
// Close all connections
|
|
313
|
+
for (const socket of this.connections.values()) {
|
|
314
|
+
socket.end();
|
|
315
|
+
}
|
|
316
|
+
this.connections.clear();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Close the transport and all connections
|
|
322
|
+
*/
|
|
323
|
+
close() {
|
|
324
|
+
// Close all connections
|
|
325
|
+
this.closeConnection();
|
|
326
|
+
|
|
327
|
+
// Close server
|
|
328
|
+
if (this.server) {
|
|
329
|
+
this.server.close();
|
|
330
|
+
this.server = null;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
this.isListening = false;
|
|
334
|
+
this.mode = null;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Bind is an alias for listen in TCP
|
|
339
|
+
*/
|
|
340
|
+
async bind(port, address) {
|
|
341
|
+
return this.listen(port, address);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* @class DataChannelTransport
|
|
347
|
+
* @description High-level transport for data channels using TCP
|
|
348
|
+
* Implements message framing with length prefix
|
|
349
|
+
*/
|
|
350
|
+
class DataChannelTransport extends EventEmitter {
|
|
351
|
+
constructor() {
|
|
352
|
+
super();
|
|
353
|
+
this.tcpTransport = new TCPTransport();
|
|
354
|
+
this.messageBuffers = new Map(); // connection -> incomplete message buffer
|
|
355
|
+
|
|
356
|
+
// Forward events
|
|
357
|
+
this.tcpTransport.on('connection', (connectionId, rinfo) => {
|
|
358
|
+
this.messageBuffers.set(connectionId, Buffer.alloc(0));
|
|
359
|
+
this.emit('connection', connectionId, rinfo);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
this.tcpTransport.on('connectionClosed', (connectionId) => {
|
|
363
|
+
this.messageBuffers.delete(connectionId);
|
|
364
|
+
this.emit('connectionClosed', connectionId);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
this.tcpTransport.on('data', (data, rinfo) => {
|
|
368
|
+
this._handleData(data, rinfo);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
this.tcpTransport.on('error', (error) => {
|
|
372
|
+
this.emit('error', error);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
this.tcpTransport.on('listening', () => {
|
|
376
|
+
this.emit('listening');
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
this.tcpTransport.on('close', () => {
|
|
380
|
+
this.emit('close');
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Handle incoming data with message framing
|
|
386
|
+
* Message format: [4 bytes length][message data]
|
|
387
|
+
* @private
|
|
388
|
+
*/
|
|
389
|
+
_handleData(data, rinfo) {
|
|
390
|
+
const connectionId = rinfo.connectionId || 'default';
|
|
391
|
+
|
|
392
|
+
// Get existing buffer or create new
|
|
393
|
+
let buffer = this.messageBuffers.get(connectionId) || Buffer.alloc(0);
|
|
394
|
+
buffer = Buffer.concat([buffer, data]);
|
|
395
|
+
this.messageBuffers.set(connectionId, buffer);
|
|
396
|
+
|
|
397
|
+
// Process complete messages
|
|
398
|
+
while (buffer.length >= 4) {
|
|
399
|
+
const messageLength = buffer.readUInt32BE(0);
|
|
400
|
+
|
|
401
|
+
if (buffer.length >= 4 + messageLength) {
|
|
402
|
+
// Extract complete message
|
|
403
|
+
const message = buffer.slice(4, 4 + messageLength);
|
|
404
|
+
buffer = buffer.slice(4 + messageLength);
|
|
405
|
+
this.messageBuffers.set(connectionId, buffer);
|
|
406
|
+
|
|
407
|
+
// Emit message event
|
|
408
|
+
this.emit('message', message, rinfo);
|
|
409
|
+
} else {
|
|
410
|
+
// Incomplete message, wait for more data
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Send message with length prefix
|
|
418
|
+
* @param {Buffer|string} message - Message to send
|
|
419
|
+
* @param {string} [connectionId] - Connection ID (for server mode)
|
|
420
|
+
* @returns {Promise<void>}
|
|
421
|
+
*/
|
|
422
|
+
async sendMessage(message, connectionId) {
|
|
423
|
+
const messageBuffer = Buffer.isBuffer(message) ? message : Buffer.from(message);
|
|
424
|
+
|
|
425
|
+
// Create framed message: [length][data]
|
|
426
|
+
const frame = Buffer.alloc(4 + messageBuffer.length);
|
|
427
|
+
frame.writeUInt32BE(messageBuffer.length, 0);
|
|
428
|
+
messageBuffer.copy(frame, 4);
|
|
429
|
+
|
|
430
|
+
if (connectionId) {
|
|
431
|
+
await this.tcpTransport.send(frame, connectionId);
|
|
432
|
+
} else {
|
|
433
|
+
// Broadcast to all connections
|
|
434
|
+
await this.tcpTransport.send(frame);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Start listening (server mode)
|
|
440
|
+
*/
|
|
441
|
+
async listen(port, address) {
|
|
442
|
+
return this.tcpTransport.listen(port, address);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Connect to remote (client mode)
|
|
447
|
+
*/
|
|
448
|
+
async connect(address, port) {
|
|
449
|
+
return this.tcpTransport.connect(address, port);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Get local address info
|
|
454
|
+
*/
|
|
455
|
+
get localAddress() {
|
|
456
|
+
return this.tcpTransport.localAddress;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
get localPort() {
|
|
460
|
+
return this.tcpTransport.localPort;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Close transport
|
|
465
|
+
*/
|
|
466
|
+
close() {
|
|
467
|
+
this.messageBuffers.clear();
|
|
468
|
+
this.tcpTransport.close();
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
module.exports = {
|
|
473
|
+
NetworkTransport,
|
|
474
|
+
UDPTransport,
|
|
475
|
+
TCPTransport,
|
|
476
|
+
DataChannelTransport,
|
|
477
|
+
TransportProtocol
|
|
478
|
+
};
|