node-rtc-connection 1.0.19 → 2.0.5
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/index.cjs +1 -0
- package/index.mjs +1 -0
- package/package.json +14 -46
- package/types/crypto/der.d.ts +107 -0
- package/types/crypto/x509.d.ts +56 -0
- package/types/datachannel/RTCDataChannel.d.ts +179 -0
- package/types/dtls/RTCCertificate.d.ts +163 -0
- package/types/dtls/cipher.d.ts +81 -0
- package/types/dtls/connection.d.ts +81 -0
- package/types/dtls/prf.d.ts +29 -0
- package/types/dtls/protocol.d.ts +127 -0
- package/types/foundation/ByteBufferQueue.d.ts +71 -0
- package/types/foundation/RTCError.d.ts +152 -0
- package/types/ice/RTCIceCandidate.d.ts +161 -0
- package/types/ice/ice-agent.d.ts +154 -0
- package/types/ice/stun-message.d.ts +92 -0
- package/types/index.d.ts +29 -0
- package/types/peerconnection/RTCPeerConnection.d.ts +74 -0
- package/types/sctp/association.d.ts +77 -0
- package/types/sctp/chunks.d.ts +200 -0
- package/types/sctp/crc32c.d.ts +24 -0
- package/types/sctp/datachannel-manager.d.ts +51 -0
- package/types/sctp/dcep.d.ts +56 -0
- package/types/sdp/RTCSessionDescription.d.ts +73 -0
- package/types/sdp/sdp-utils.d.ts +103 -0
- package/types/stun/stun-client.d.ts +119 -0
- package/types/transport-stack.d.ts +68 -0
- package/dist/index.cjs +0 -5618
- package/dist/index.cjs.map +0 -1
- package/dist/index.mjs +0 -5616
- 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/foundation/ByteBufferQueue.js +0 -235
- package/src/foundation/RTCError.js +0 -226
- package/src/ice/RTCIceCandidate.js +0 -301
- 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/RTCSessionDescription.js +0 -102
- package/src/sdp/sdp-utils.js +0 -224
- package/src/stun/stun-client.js +0 -777
|
@@ -1,354 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file RTCDataChannel.js
|
|
3
|
-
* @description WebRTC DataChannel implementation for peer-to-peer data transfer.
|
|
4
|
-
* @module datachannel/RTCDataChannel
|
|
5
|
-
*
|
|
6
|
-
* Ported from Chromium's RTCDataChannel implementation:
|
|
7
|
-
* - cc/rtc_data_channel.h
|
|
8
|
-
* - cc/rtc_data_channel.cc
|
|
9
|
-
* - cc/rtc_data_channel.idl
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
const EventEmitter = require('events');
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* RTCDataChannelState - Current state of the data channel
|
|
16
|
-
* @readonly
|
|
17
|
-
* @enum {string}
|
|
18
|
-
*/
|
|
19
|
-
const RTCDataChannelState = Object.freeze({
|
|
20
|
-
CONNECTING: 'connecting',
|
|
21
|
-
OPEN: 'open',
|
|
22
|
-
CLOSING: 'closing',
|
|
23
|
-
CLOSED: 'closed'
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* RTCDataChannelInit - Configuration for creating a data channel
|
|
28
|
-
* @typedef {Object} RTCDataChannelInit
|
|
29
|
-
* @property {boolean} [ordered=true] - Whether messages must arrive in order
|
|
30
|
-
* @property {number} [maxPacketLifeTime] - Maximum packet lifetime in milliseconds
|
|
31
|
-
* @property {number} [maxRetransmits] - Maximum number of retransmissions
|
|
32
|
-
* @property {string} [protocol=''] - Subprotocol name
|
|
33
|
-
* @property {boolean} [negotiated=false] - Whether channel was negotiated out-of-band
|
|
34
|
-
* @property {number} [id] - Channel ID (required if negotiated is true)
|
|
35
|
-
*/
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* @class RTCDataChannel
|
|
39
|
-
* @extends EventEmitter
|
|
40
|
-
* @description Represents a bidirectional data channel between peers.
|
|
41
|
-
* Provides reliable or unreliable data transfer with configurable ordering.
|
|
42
|
-
*
|
|
43
|
-
* Events:
|
|
44
|
-
* - 'open': Fired when the channel opens
|
|
45
|
-
* - 'message': Fired when a message is received
|
|
46
|
-
* - 'bufferedamountlow': Fired when bufferedAmount drops below threshold
|
|
47
|
-
* - 'error': Fired when an error occurs
|
|
48
|
-
* - 'closing': Fired when the channel is closing
|
|
49
|
-
* - 'close': Fired when the channel closes
|
|
50
|
-
*
|
|
51
|
-
* @example
|
|
52
|
-
* const dataChannel = peerConnection.createDataChannel('myChannel', {
|
|
53
|
-
* ordered: true,
|
|
54
|
-
* maxRetransmits: 3
|
|
55
|
-
* });
|
|
56
|
-
*
|
|
57
|
-
* dataChannel.on('open', () => {
|
|
58
|
-
* console.log('Channel opened');
|
|
59
|
-
* dataChannel.send('Hello!');
|
|
60
|
-
* });
|
|
61
|
-
*
|
|
62
|
-
* dataChannel.on('message', (event) => {
|
|
63
|
-
* console.log('Received:', event.data);
|
|
64
|
-
* });
|
|
65
|
-
*/
|
|
66
|
-
class RTCDataChannel extends EventEmitter {
|
|
67
|
-
/**
|
|
68
|
-
* Create an RTCDataChannel instance.
|
|
69
|
-
* @param {string} label - Channel label
|
|
70
|
-
* @param {RTCDataChannelInit} [init] - Channel configuration
|
|
71
|
-
*/
|
|
72
|
-
constructor(label, init = {}) {
|
|
73
|
-
super();
|
|
74
|
-
|
|
75
|
-
if (typeof label !== 'string') {
|
|
76
|
-
throw new TypeError('label must be a string');
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Channel configuration
|
|
80
|
-
this._label = label;
|
|
81
|
-
this._ordered = init.ordered !== undefined ? init.ordered : true;
|
|
82
|
-
this._maxPacketLifeTime = init.maxPacketLifeTime || null;
|
|
83
|
-
this._maxRetransmits = init.maxRetransmits || null;
|
|
84
|
-
this._protocol = init.protocol || '';
|
|
85
|
-
this._negotiated = init.negotiated || false;
|
|
86
|
-
this._id = init.id !== undefined ? init.id : null;
|
|
87
|
-
|
|
88
|
-
// State
|
|
89
|
-
this._readyState = RTCDataChannelState.CONNECTING;
|
|
90
|
-
this._bufferedAmount = 0;
|
|
91
|
-
this._bufferedAmountLowThreshold = 0;
|
|
92
|
-
this._binaryType = 'arraybuffer'; // or 'blob'
|
|
93
|
-
|
|
94
|
-
// Message queue for when not open
|
|
95
|
-
this._messageQueue = [];
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Get the channel label.
|
|
100
|
-
* @returns {string} Channel label
|
|
101
|
-
*/
|
|
102
|
-
get label() {
|
|
103
|
-
return this._label;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Check if messages are delivered in order.
|
|
108
|
-
* @returns {boolean} True if ordered
|
|
109
|
-
*/
|
|
110
|
-
get ordered() {
|
|
111
|
-
return this._ordered;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Get the maximum packet lifetime in milliseconds.
|
|
116
|
-
* @returns {number|null} Maximum lifetime or null if not set
|
|
117
|
-
*/
|
|
118
|
-
get maxPacketLifeTime() {
|
|
119
|
-
return this._maxPacketLifeTime;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Get the maximum number of retransmissions.
|
|
124
|
-
* @returns {number|null} Maximum retransmits or null if not set
|
|
125
|
-
*/
|
|
126
|
-
get maxRetransmits() {
|
|
127
|
-
return this._maxRetransmits;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Get the subprotocol name.
|
|
132
|
-
* @returns {string} Protocol name
|
|
133
|
-
*/
|
|
134
|
-
get protocol() {
|
|
135
|
-
return this._protocol;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Check if the channel was negotiated out-of-band.
|
|
140
|
-
* @returns {boolean} True if negotiated
|
|
141
|
-
*/
|
|
142
|
-
get negotiated() {
|
|
143
|
-
return this._negotiated;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Get the channel ID.
|
|
148
|
-
* @returns {number|null} Channel ID or null if not assigned
|
|
149
|
-
*/
|
|
150
|
-
get id() {
|
|
151
|
-
return this._id;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Get the current state of the channel.
|
|
156
|
-
* @returns {string} Channel state
|
|
157
|
-
*/
|
|
158
|
-
get readyState() {
|
|
159
|
-
return this._readyState;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Get the number of bytes queued to send.
|
|
164
|
-
* @returns {number} Buffered amount in bytes
|
|
165
|
-
*/
|
|
166
|
-
get bufferedAmount() {
|
|
167
|
-
return this._bufferedAmount;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Get the threshold for bufferedamountlow event.
|
|
172
|
-
* @returns {number} Threshold in bytes
|
|
173
|
-
*/
|
|
174
|
-
get bufferedAmountLowThreshold() {
|
|
175
|
-
return this._bufferedAmountLowThreshold;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Set the threshold for bufferedamountlow event.
|
|
180
|
-
* @param {number} value - Threshold in bytes
|
|
181
|
-
*/
|
|
182
|
-
set bufferedAmountLowThreshold(value) {
|
|
183
|
-
this._bufferedAmountLowThreshold = value;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Get the binary data type.
|
|
188
|
-
* @returns {string} 'arraybuffer' or 'blob'
|
|
189
|
-
*/
|
|
190
|
-
get binaryType() {
|
|
191
|
-
return this._binaryType;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Set the binary data type.
|
|
196
|
-
* @param {string} value - 'arraybuffer' or 'blob'
|
|
197
|
-
* @throws {TypeError} If value is invalid
|
|
198
|
-
*/
|
|
199
|
-
set binaryType(value) {
|
|
200
|
-
if (value !== 'arraybuffer' && value !== 'blob') {
|
|
201
|
-
throw new TypeError('binaryType must be "arraybuffer" or "blob"');
|
|
202
|
-
}
|
|
203
|
-
this._binaryType = value;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Check if the channel is reliable (deprecated).
|
|
208
|
-
* @returns {boolean} True if ordered and no packet lifetime/retransmit limits
|
|
209
|
-
* @deprecated Use ordered, maxPacketLifeTime, and maxRetransmits instead
|
|
210
|
-
*/
|
|
211
|
-
get reliable() {
|
|
212
|
-
return this._ordered &&
|
|
213
|
-
this._maxPacketLifeTime === null &&
|
|
214
|
-
this._maxRetransmits === null;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Send a message through the channel.
|
|
219
|
-
* @param {string|ArrayBuffer|ArrayBufferView|Blob} data - Data to send
|
|
220
|
-
* @throws {Error} If channel is not open or data is invalid
|
|
221
|
-
*/
|
|
222
|
-
send(data) {
|
|
223
|
-
if (this._readyState !== RTCDataChannelState.OPEN) {
|
|
224
|
-
throw new Error('RTCDataChannel.readyState is not "open"');
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
let dataToSend;
|
|
228
|
-
let byteLength = 0;
|
|
229
|
-
|
|
230
|
-
if (typeof data === 'string') {
|
|
231
|
-
dataToSend = Buffer.from(data, 'utf8');
|
|
232
|
-
byteLength = dataToSend.length;
|
|
233
|
-
} else if (data instanceof ArrayBuffer) {
|
|
234
|
-
dataToSend = Buffer.from(data);
|
|
235
|
-
byteLength = data.byteLength;
|
|
236
|
-
} else if (ArrayBuffer.isView(data)) {
|
|
237
|
-
dataToSend = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
238
|
-
byteLength = data.byteLength;
|
|
239
|
-
} else if (data && typeof data.arrayBuffer === 'function') {
|
|
240
|
-
// Blob-like object
|
|
241
|
-
throw new Error('Blob sending not yet implemented');
|
|
242
|
-
} else {
|
|
243
|
-
throw new TypeError('Invalid data type');
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Update buffered amount
|
|
247
|
-
this._bufferedAmount += byteLength;
|
|
248
|
-
|
|
249
|
-
// Use real network transport
|
|
250
|
-
if (!this._send) {
|
|
251
|
-
throw new Error('Data channel not connected to network transport');
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
this._send(data).catch(err => {
|
|
255
|
-
console.error('Send error:', err);
|
|
256
|
-
this.emit('error', err);
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Emit bufferedamountlow if appropriate
|
|
262
|
-
* @private
|
|
263
|
-
*/
|
|
264
|
-
_emitBufferedAmountLow() {
|
|
265
|
-
if (this._bufferedAmount <= this._bufferedAmountLowThreshold) {
|
|
266
|
-
this.emit('bufferedamountlow');
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Close the data channel.
|
|
272
|
-
*/
|
|
273
|
-
close() {
|
|
274
|
-
if (this._readyState === RTCDataChannelState.CLOSING ||
|
|
275
|
-
this._readyState === RTCDataChannelState.CLOSED) {
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
this._setState(RTCDataChannelState.CLOSING);
|
|
280
|
-
|
|
281
|
-
// Transition to closed asynchronously
|
|
282
|
-
setImmediate(() => {
|
|
283
|
-
if (this._readyState === RTCDataChannelState.CLOSING) {
|
|
284
|
-
this._setState(RTCDataChannelState.CLOSED);
|
|
285
|
-
}
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Set the channel state and emit appropriate events.
|
|
291
|
-
* @param {string} newState - New state
|
|
292
|
-
* @private
|
|
293
|
-
*/
|
|
294
|
-
_setState(newState) {
|
|
295
|
-
const oldState = this._readyState;
|
|
296
|
-
if (oldState === newState) {
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
this._readyState = newState;
|
|
301
|
-
|
|
302
|
-
// Emit state-specific events
|
|
303
|
-
if (newState === RTCDataChannelState.OPEN) {
|
|
304
|
-
this.emit('open');
|
|
305
|
-
} else if (newState === RTCDataChannelState.CLOSING) {
|
|
306
|
-
this.emit('closing');
|
|
307
|
-
} else if (newState === RTCDataChannelState.CLOSED) {
|
|
308
|
-
this.emit('close');
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* Set channel to open state (internal use).
|
|
314
|
-
* @private
|
|
315
|
-
*/
|
|
316
|
-
_setStateToOpen() {
|
|
317
|
-
this._setState(RTCDataChannelState.OPEN);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* Handle received message (internal use).
|
|
322
|
-
* @param {Buffer|string} data - Received data
|
|
323
|
-
* @private
|
|
324
|
-
*/
|
|
325
|
-
_onMessage(data) {
|
|
326
|
-
const event = {
|
|
327
|
-
data: data
|
|
328
|
-
};
|
|
329
|
-
this.emit('message', event);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
/**
|
|
333
|
-
* Receive message from network transport (internal use).
|
|
334
|
-
* @param {any} data - Received data
|
|
335
|
-
* @private
|
|
336
|
-
*/
|
|
337
|
-
_receiveMessage(data) {
|
|
338
|
-
this._onMessage(data);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* Set the channel ID (internal use).
|
|
343
|
-
* @param {number} id - Channel ID
|
|
344
|
-
* @private
|
|
345
|
-
*/
|
|
346
|
-
_setId(id) {
|
|
347
|
-
this._id = id;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
module.exports = {
|
|
352
|
-
RTCDataChannel,
|
|
353
|
-
RTCDataChannelState
|
|
354
|
-
};
|
|
@@ -1,310 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file RTCCertificate.js
|
|
3
|
-
* @description DTLS certificate implementation for WebRTC.
|
|
4
|
-
* @module dtls/RTCCertificate
|
|
5
|
-
*
|
|
6
|
-
* Ported from Chromium's RTCCertificate implementation:
|
|
7
|
-
* - cc/rtc_certificate.h
|
|
8
|
-
* - cc/rtc_certificate.cc
|
|
9
|
-
* - cc/rtc_certificate.idl
|
|
10
|
-
* - cc/rtc_certificate_generator.h
|
|
11
|
-
* - cc/rtc_certificate_generator.cc
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
const crypto = require('crypto');
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* RTCDtlsFingerprint - DTLS certificate fingerprint
|
|
18
|
-
* @typedef {Object} RTCDtlsFingerprint
|
|
19
|
-
* @property {string} algorithm - Hash algorithm (e.g., 'sha-256')
|
|
20
|
-
* @property {string} value - Fingerprint value (colon-separated hex)
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Generate a self-signed certificate for DTLS.
|
|
25
|
-
* @param {Object} options - Certificate generation options
|
|
26
|
-
* @param {string} [options.name='webrtc'] - Common name for the certificate
|
|
27
|
-
* @param {number} [options.days=30] - Days until expiration
|
|
28
|
-
* @param {string} [options.hash='sha256'] - Hash algorithm
|
|
29
|
-
* @returns {Object} Certificate object with key and cert
|
|
30
|
-
* @private
|
|
31
|
-
*/
|
|
32
|
-
function generateSelfSignedCertificate(options = {}) {
|
|
33
|
-
const {
|
|
34
|
-
name = 'webrtc',
|
|
35
|
-
days = 30,
|
|
36
|
-
hash = 'sha256'
|
|
37
|
-
} = options;
|
|
38
|
-
|
|
39
|
-
// Generate RSA key pair
|
|
40
|
-
const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
|
|
41
|
-
modulusLength: 2048,
|
|
42
|
-
publicKeyEncoding: {
|
|
43
|
-
type: 'spki',
|
|
44
|
-
format: 'pem'
|
|
45
|
-
},
|
|
46
|
-
privateKeyEncoding: {
|
|
47
|
-
type: 'pkcs8',
|
|
48
|
-
format: 'pem'
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
// Create certificate using Node.js crypto
|
|
53
|
-
// Note: Node.js doesn't have built-in self-signed cert generation,
|
|
54
|
-
// so we'll use a simplified approach storing the key pair
|
|
55
|
-
const expirationDate = new Date();
|
|
56
|
-
expirationDate.setDate(expirationDate.getDate() + days);
|
|
57
|
-
|
|
58
|
-
return {
|
|
59
|
-
privateKey,
|
|
60
|
-
publicKey,
|
|
61
|
-
expires: expirationDate.getTime(),
|
|
62
|
-
hash
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Calculate fingerprint from public key.
|
|
68
|
-
* @param {string} publicKey - PEM-encoded public key
|
|
69
|
-
* @param {string} algorithm - Hash algorithm (e.g., 'sha256')
|
|
70
|
-
* @returns {string} Fingerprint (colon-separated hex)
|
|
71
|
-
* @private
|
|
72
|
-
*/
|
|
73
|
-
function calculateFingerprint(publicKey, algorithm = 'sha256') {
|
|
74
|
-
// Remove PEM headers and decode base64
|
|
75
|
-
const pemBody = publicKey
|
|
76
|
-
.replace(/-----BEGIN PUBLIC KEY-----/, '')
|
|
77
|
-
.replace(/-----END PUBLIC KEY-----/, '')
|
|
78
|
-
.replace(/\s/g, '');
|
|
79
|
-
|
|
80
|
-
const keyBuffer = Buffer.from(pemBody, 'base64');
|
|
81
|
-
|
|
82
|
-
// Calculate hash
|
|
83
|
-
const hash = crypto.createHash(algorithm);
|
|
84
|
-
hash.update(keyBuffer);
|
|
85
|
-
const digest = hash.digest('hex').toUpperCase();
|
|
86
|
-
|
|
87
|
-
// Format as colon-separated pairs
|
|
88
|
-
return digest.match(/.{2}/g).join(':');
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* @class RTCCertificate
|
|
93
|
-
* @description Represents a certificate used for DTLS in WebRTC.
|
|
94
|
-
* The certificate includes a key pair and expiration time.
|
|
95
|
-
*
|
|
96
|
-
* @example
|
|
97
|
-
* // Generate a certificate
|
|
98
|
-
* const cert = await RTCCertificate.generateCertificate();
|
|
99
|
-
* console.log('Expires:', new Date(cert.expires));
|
|
100
|
-
* console.log('Fingerprints:', cert.getFingerprints());
|
|
101
|
-
*
|
|
102
|
-
* @example
|
|
103
|
-
* // Generate with custom expiration
|
|
104
|
-
* const cert = await RTCCertificate.generateCertificate({
|
|
105
|
-
* name: 'my-peer',
|
|
106
|
-
* expires: Date.now() + (90 * 24 * 60 * 60 * 1000) // 90 days
|
|
107
|
-
* });
|
|
108
|
-
*/
|
|
109
|
-
class RTCCertificate {
|
|
110
|
-
/**
|
|
111
|
-
* Create an RTCCertificate instance.
|
|
112
|
-
* Use generateCertificate() static method instead of calling directly.
|
|
113
|
-
* @param {Object} certData - Internal certificate data
|
|
114
|
-
* @private
|
|
115
|
-
*/
|
|
116
|
-
constructor(certData) {
|
|
117
|
-
// Store certificate data
|
|
118
|
-
this._privateKey = certData.privateKey;
|
|
119
|
-
this._publicKey = certData.publicKey;
|
|
120
|
-
this._expires = certData.expires;
|
|
121
|
-
this._hash = certData.hash || 'sha256';
|
|
122
|
-
|
|
123
|
-
// Cache fingerprints
|
|
124
|
-
this._fingerprints = null;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Get the expiration time.
|
|
129
|
-
* @returns {number} Expiration time in milliseconds since epoch (DOMTimeStamp)
|
|
130
|
-
*/
|
|
131
|
-
get expires() {
|
|
132
|
-
return this._expires;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Get the certificate fingerprints.
|
|
137
|
-
* Returns an array of fingerprints for the certificate chain.
|
|
138
|
-
* For self-signed certificates, this returns a single fingerprint.
|
|
139
|
-
*
|
|
140
|
-
* @returns {Array<RTCDtlsFingerprint>} Array of fingerprint objects
|
|
141
|
-
*/
|
|
142
|
-
getFingerprints() {
|
|
143
|
-
if (!this._fingerprints) {
|
|
144
|
-
// Calculate fingerprint for multiple algorithms
|
|
145
|
-
const algorithms = ['sha-256', 'sha-384', 'sha-512'];
|
|
146
|
-
this._fingerprints = algorithms.map(algorithm => {
|
|
147
|
-
const hashAlgo = algorithm.replace('-', ''); // 'sha-256' -> 'sha256'
|
|
148
|
-
return {
|
|
149
|
-
algorithm,
|
|
150
|
-
value: calculateFingerprint(this._publicKey, hashAlgo)
|
|
151
|
-
};
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return this._fingerprints.map(fp => ({ ...fp }));
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Get the private key in PEM format.
|
|
160
|
-
* @returns {string} PEM-encoded private key
|
|
161
|
-
* @internal
|
|
162
|
-
*/
|
|
163
|
-
getPrivateKey() {
|
|
164
|
-
return this._privateKey;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Get the public key in PEM format.
|
|
169
|
-
* @returns {string} PEM-encoded public key
|
|
170
|
-
* @internal
|
|
171
|
-
*/
|
|
172
|
-
getPublicKey() {
|
|
173
|
-
return this._publicKey;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Convert to PEM format (for serialization/storage).
|
|
178
|
-
* @returns {Object} Object with pemPrivateKey and pemCertificate
|
|
179
|
-
*/
|
|
180
|
-
toPEM() {
|
|
181
|
-
return {
|
|
182
|
-
pemPrivateKey: this._privateKey,
|
|
183
|
-
pemCertificate: this._publicKey
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Check if the certificate has expired.
|
|
189
|
-
* @returns {boolean} True if expired, false otherwise
|
|
190
|
-
*/
|
|
191
|
-
isExpired() {
|
|
192
|
-
return Date.now() > this._expires;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Generate a new RTCCertificate asynchronously.
|
|
197
|
-
*
|
|
198
|
-
* @param {Object} [options] - Generation options
|
|
199
|
-
* @param {string} [options.name='webrtc'] - Common name for the certificate
|
|
200
|
-
* @param {number} [options.expires] - Expiration time in ms (default: 30 days from now)
|
|
201
|
-
* @param {string} [options.hash='sha256'] - Hash algorithm
|
|
202
|
-
* @returns {Promise<RTCCertificate>} Promise resolving to generated certificate
|
|
203
|
-
*
|
|
204
|
-
* @example
|
|
205
|
-
* const cert = await RTCCertificate.generateCertificate({
|
|
206
|
-
* name: 'my-app',
|
|
207
|
-
* expires: Date.now() + (90 * 24 * 60 * 60 * 1000) // 90 days
|
|
208
|
-
* });
|
|
209
|
-
*/
|
|
210
|
-
static async generateCertificate(options = {}) {
|
|
211
|
-
return new Promise((resolve, reject) => {
|
|
212
|
-
try {
|
|
213
|
-
// Calculate expiration
|
|
214
|
-
let expires;
|
|
215
|
-
if (options.expires) {
|
|
216
|
-
expires = options.expires;
|
|
217
|
-
} else {
|
|
218
|
-
const days = options.days || 30;
|
|
219
|
-
expires = Date.now() + (days * 24 * 60 * 60 * 1000);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Generate certificate in next tick to avoid blocking
|
|
223
|
-
setImmediate(() => {
|
|
224
|
-
try {
|
|
225
|
-
const certData = generateSelfSignedCertificate({
|
|
226
|
-
name: options.name || 'webrtc',
|
|
227
|
-
days: Math.ceil((expires - Date.now()) / (24 * 60 * 60 * 1000)),
|
|
228
|
-
hash: options.hash || 'sha256'
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
certData.expires = expires;
|
|
232
|
-
const certificate = new RTCCertificate(certData);
|
|
233
|
-
resolve(certificate);
|
|
234
|
-
} catch (err) {
|
|
235
|
-
reject(err);
|
|
236
|
-
}
|
|
237
|
-
});
|
|
238
|
-
} catch (err) {
|
|
239
|
-
reject(err);
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Create a certificate from PEM strings.
|
|
246
|
-
*
|
|
247
|
-
* @param {string} pemPrivateKey - PEM-encoded private key
|
|
248
|
-
* @param {string} pemCertificate - PEM-encoded certificate (or public key)
|
|
249
|
-
* @param {number} [expires] - Expiration time in ms (default: 30 days from now)
|
|
250
|
-
* @returns {RTCCertificate} Certificate instance
|
|
251
|
-
*
|
|
252
|
-
* @example
|
|
253
|
-
* const cert = RTCCertificate.fromPEM(
|
|
254
|
-
* privateKeyPEM,
|
|
255
|
-
* publicKeyPEM,
|
|
256
|
-
* Date.now() + (30 * 24 * 60 * 60 * 1000)
|
|
257
|
-
* );
|
|
258
|
-
*/
|
|
259
|
-
static fromPEM(pemPrivateKey, pemCertificate, expires) {
|
|
260
|
-
if (typeof pemPrivateKey !== 'string' || pemPrivateKey.length === 0) {
|
|
261
|
-
throw new TypeError('pemPrivateKey must be a non-empty string');
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
if (typeof pemCertificate !== 'string' || pemCertificate.length === 0) {
|
|
265
|
-
throw new TypeError('pemCertificate must be a non-empty string');
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Default expiration to 30 days if not provided
|
|
269
|
-
const expirationTime = expires || (Date.now() + (30 * 24 * 60 * 60 * 1000));
|
|
270
|
-
|
|
271
|
-
return new RTCCertificate({
|
|
272
|
-
privateKey: pemPrivateKey,
|
|
273
|
-
publicKey: pemCertificate,
|
|
274
|
-
expires: expirationTime,
|
|
275
|
-
hash: 'sha256'
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* Check if key parameters are supported.
|
|
281
|
-
* Currently supports RSA with 1024-4096 bits and ECDSA.
|
|
282
|
-
*
|
|
283
|
-
* @param {Object} keyParams - Key parameters
|
|
284
|
-
* @param {string} keyParams.type - Key type ('RSA' or 'ECDSA')
|
|
285
|
-
* @param {number} [keyParams.rsaModulusLength] - RSA key size in bits
|
|
286
|
-
* @param {string} [keyParams.namedCurve] - ECDSA curve name
|
|
287
|
-
* @returns {boolean} True if supported, false otherwise
|
|
288
|
-
*/
|
|
289
|
-
static isSupportedKeyParams(keyParams) {
|
|
290
|
-
if (!keyParams || typeof keyParams !== 'object') {
|
|
291
|
-
return false;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
if (keyParams.type === 'RSA') {
|
|
295
|
-
const modulusLength = keyParams.rsaModulusLength || 2048;
|
|
296
|
-
// Support 1024 to 4096 bits
|
|
297
|
-
return modulusLength >= 1024 && modulusLength <= 4096;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
if (keyParams.type === 'ECDSA') {
|
|
301
|
-
// Support common ECDSA curves
|
|
302
|
-
const curve = keyParams.namedCurve;
|
|
303
|
-
return ['P-256', 'P-384', 'P-521'].includes(curve);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
return false;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
module.exports = RTCCertificate;
|