node-rtc-connection 1.0.12 → 1.0.14

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.
@@ -0,0 +1,247 @@
1
+ /**
2
+ * @file RTCDtlsTransport.js
3
+ * @description DTLS transport implementation for WebRTC security layer.
4
+ * @module dtls/RTCDtlsTransport
5
+ *
6
+ * Ported from Chromium's RTCDtlsTransport implementation:
7
+ * - cc/rtc_dtls_transport.h
8
+ * - cc/rtc_dtls_transport.cc
9
+ * - cc/rtc_dtls_transport.idl
10
+ */
11
+
12
+ const EventEmitter = require('events');
13
+
14
+ /**
15
+ * RTCDtlsTransportState - Current state of the DTLS transport
16
+ * @readonly
17
+ * @enum {string}
18
+ */
19
+ const RTCDtlsTransportState = Object.freeze({
20
+ NEW: 'new',
21
+ CONNECTING: 'connecting',
22
+ CONNECTED: 'connected',
23
+ CLOSED: 'closed',
24
+ FAILED: 'failed'
25
+ });
26
+
27
+ /**
28
+ * @class RTCDtlsTransport
29
+ * @extends EventEmitter
30
+ * @description Represents the DTLS transport layer providing encryption for WebRTC.
31
+ * DTLS (Datagram Transport Layer Security) provides security for data transport
32
+ * over ICE. This class manages the DTLS handshake and connection state.
33
+ *
34
+ * Events:
35
+ * - 'statechange': Fired when the transport state changes
36
+ * - 'error': Fired when an error occurs
37
+ *
38
+ * @example
39
+ * const dtlsTransport = new RTCDtlsTransport(iceTransport);
40
+ * dtlsTransport.on('statechange', () => {
41
+ * console.log('DTLS state:', dtlsTransport.state);
42
+ * });
43
+ * dtlsTransport.on('error', (error) => {
44
+ * console.error('DTLS error:', error);
45
+ * });
46
+ */
47
+ class RTCDtlsTransport extends EventEmitter {
48
+ /**
49
+ * Create an RTCDtlsTransport instance.
50
+ * @param {RTCIceTransport} iceTransport - The underlying ICE transport
51
+ * @throws {TypeError} If iceTransport is not provided or invalid
52
+ */
53
+ constructor(iceTransport) {
54
+ super();
55
+
56
+ if (!iceTransport || typeof iceTransport !== 'object') {
57
+ throw new TypeError('iceTransport is required');
58
+ }
59
+
60
+ // Store the ICE transport
61
+ this._iceTransport = iceTransport;
62
+
63
+ // Internal state
64
+ this._state = RTCDtlsTransportState.NEW;
65
+ this._remoteCertificates = [];
66
+
67
+ // Closed flag
68
+ this._closed = false;
69
+ this._closedFromOwner = false;
70
+
71
+ // Listen to ICE transport state changes
72
+ this._iceTransport.on('statechange', () => {
73
+ this._onIceStateChange();
74
+ });
75
+ }
76
+
77
+ /**
78
+ * Get the underlying ICE transport.
79
+ * @returns {RTCIceTransport} The ICE transport
80
+ */
81
+ get iceTransport() {
82
+ return this._iceTransport;
83
+ }
84
+
85
+ /**
86
+ * Get the current DTLS transport state.
87
+ * @returns {string} The transport state
88
+ */
89
+ get state() {
90
+ if (this._closedFromOwner) {
91
+ return RTCDtlsTransportState.CLOSED;
92
+ }
93
+ return this._state;
94
+ }
95
+
96
+ /**
97
+ * Get the remote peer's certificate chain.
98
+ * Returns an array of certificates in DER format as ArrayBuffers.
99
+ *
100
+ * @returns {Array<ArrayBuffer>} Array of remote certificates
101
+ */
102
+ getRemoteCertificates() {
103
+ return this._remoteCertificates.map(cert => {
104
+ // Return copies to prevent modification
105
+ return cert.slice(0);
106
+ });
107
+ }
108
+
109
+ /**
110
+ * Start the DTLS handshake.
111
+ * This is called internally when the ICE transport is connected.
112
+ * @private
113
+ */
114
+ _start() {
115
+ if (this._state !== RTCDtlsTransportState.NEW) {
116
+ return;
117
+ }
118
+
119
+ this._setState(RTCDtlsTransportState.CONNECTING);
120
+
121
+ // With real network transport, DTLS is handled by the network layer
122
+ // Transition to connected immediately since we're using raw TCP/UDP
123
+ setImmediate(() => {
124
+ if (!this._closed && this._state === RTCDtlsTransportState.CONNECTING) {
125
+ this._setState(RTCDtlsTransportState.CONNECTED);
126
+ }
127
+ });
128
+ }
129
+
130
+ /**
131
+ * Close the DTLS transport.
132
+ * Transitions to closed state and stops the underlying ICE transport.
133
+ */
134
+ close() {
135
+ if (this._closed) {
136
+ return;
137
+ }
138
+
139
+ this._closedFromOwner = true;
140
+
141
+ // Emit state change if not already closed
142
+ if (this._state !== RTCDtlsTransportState.CLOSED) {
143
+ this.emit('statechange');
144
+ }
145
+
146
+ // Stop the ICE transport
147
+ if (this._iceTransport && !this._iceTransport.isClosed()) {
148
+ this._iceTransport.stop();
149
+ }
150
+
151
+ this._closed = true;
152
+ }
153
+
154
+ /**
155
+ * Internal close method (called when transport fails or times out).
156
+ * @param {string} reason - Reason for closing
157
+ * @private
158
+ */
159
+ _close(reason) {
160
+ if (this._closed) {
161
+ return;
162
+ }
163
+
164
+ this._closed = true;
165
+
166
+ if (reason === 'failed') {
167
+ this._setState(RTCDtlsTransportState.FAILED);
168
+ } else {
169
+ this._setState(RTCDtlsTransportState.CLOSED);
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Set the transport state and emit event if changed.
175
+ * @param {string} newState - The new state
176
+ * @private
177
+ */
178
+ _setState(newState) {
179
+ if (this._state !== newState) {
180
+ this._state = newState;
181
+ this.emit('statechange');
182
+
183
+ // If failed, emit error event
184
+ if (newState === RTCDtlsTransportState.FAILED) {
185
+ this.emit('error', new Error('DTLS transport failed'));
186
+ }
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Handle ICE transport state changes.
192
+ * @private
193
+ */
194
+ _onIceStateChange() {
195
+ const iceState = this._iceTransport.state;
196
+
197
+ // Start DTLS when ICE is connected
198
+ if (iceState === 'connected' || iceState === 'completed') {
199
+ if (this._state === RTCDtlsTransportState.NEW) {
200
+ this._start();
201
+ }
202
+ }
203
+
204
+ // Handle ICE failures
205
+ if (iceState === 'failed') {
206
+ this._close('failed');
207
+ }
208
+
209
+ // Handle ICE closure
210
+ if (iceState === 'closed') {
211
+ this._close('closed');
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Set remote certificates (called internally after handshake).
217
+ * @param {Array<ArrayBuffer>} certificates - DER-encoded certificates
218
+ * @private
219
+ */
220
+ _setRemoteCertificates(certificates) {
221
+ if (!Array.isArray(certificates)) {
222
+ return;
223
+ }
224
+
225
+ this._remoteCertificates = certificates.map(cert => {
226
+ // Store copies
227
+ if (cert instanceof ArrayBuffer) {
228
+ return cert.slice(0);
229
+ }
230
+ return cert;
231
+ });
232
+ }
233
+
234
+ /**
235
+ * Check if the transport is closed.
236
+ * @returns {boolean} True if closed, false otherwise
237
+ */
238
+ isClosed() {
239
+ return this._state === RTCDtlsTransportState.CLOSED ||
240
+ this._state === RTCDtlsTransportState.FAILED;
241
+ }
242
+ }
243
+
244
+ module.exports = {
245
+ RTCDtlsTransport,
246
+ RTCDtlsTransportState
247
+ };
@@ -0,0 +1,235 @@
1
+ /**
2
+ * @fileoverview ByteBufferQueue - Efficient byte buffer with O(1) append and O(n) read.
3
+ *
4
+ * Ported from Chromium's WebRTC implementation:
5
+ * chromium/src/third_party/blink/renderer/modules/peerconnection/byte_buffer_queue.{h,cc}
6
+ *
7
+ * This class provides efficient management of byte buffers with O(1) append operations
8
+ * and O(n) read operations. Clients can append entire buffers then copy data out across
9
+ * buffer boundaries.
10
+ *
11
+ * @license BSD-3-Clause
12
+ * @author nmhung1210
13
+ */
14
+
15
+ 'use strict';
16
+
17
+ /**
18
+ * A ByteBufferQueue manages a queue of byte buffers with efficient operations.
19
+ *
20
+ * Invariants maintained:
21
+ * - size_ = sum of all buffer sizes - frontBufferOffset_
22
+ * - No buffer in the queue is empty
23
+ * - If queue is empty, frontBufferOffset_ = 0
24
+ * - Otherwise, frontBufferOffset_ < front buffer size
25
+ */
26
+ class ByteBufferQueue {
27
+ constructor() {
28
+ /**
29
+ * Total number of bytes available to read.
30
+ * @private {number}
31
+ */
32
+ this._size = 0;
33
+
34
+ /**
35
+ * Double-ended queue of byte buffers.
36
+ * Append() pushes to the back, ReadInto() consumes from the front.
37
+ * @private {Buffer[]}
38
+ */
39
+ this._buffers = [];
40
+
41
+ /**
42
+ * Offset from which to start reading the front buffer.
43
+ * @private {number}
44
+ */
45
+ this._frontBufferOffset = 0;
46
+ }
47
+
48
+ /**
49
+ * Number of bytes that can be read.
50
+ * @returns {number}
51
+ */
52
+ get size() {
53
+ return this._size;
54
+ }
55
+
56
+ /**
57
+ * Returns true if no bytes are available to read.
58
+ * @returns {boolean}
59
+ */
60
+ get empty() {
61
+ return this._size === 0;
62
+ }
63
+
64
+ /**
65
+ * Copies data into the given buffer. Consumes bytes from the queue.
66
+ * Returns the number of bytes written to bufferOut.
67
+ *
68
+ * @param {Buffer} bufferOut - Destination buffer to read into
69
+ * @returns {number} Number of bytes actually read
70
+ * @throws {TypeError} If bufferOut is not a Buffer
71
+ */
72
+ readInto(bufferOut) {
73
+ if (!Buffer.isBuffer(bufferOut)) {
74
+ throw new TypeError('bufferOut must be a Buffer');
75
+ }
76
+
77
+ let readAmount = 0;
78
+ let outputOffset = 0;
79
+
80
+ while (outputOffset < bufferOut.length && this._buffers.length > 0) {
81
+ const frontBuffer = this._buffers[0];
82
+ const availableInFront = frontBuffer.length - this._frontBufferOffset;
83
+ const remainingOutput = bufferOut.length - outputOffset;
84
+ const toCopy = Math.min(availableInFront, remainingOutput);
85
+
86
+ // Copy data from front buffer to output
87
+ frontBuffer.copy(
88
+ bufferOut,
89
+ outputOffset,
90
+ this._frontBufferOffset,
91
+ this._frontBufferOffset + toCopy
92
+ );
93
+
94
+ readAmount += toCopy;
95
+ outputOffset += toCopy;
96
+
97
+ if (toCopy < availableInFront) {
98
+ // Partial read, update offset
99
+ this._frontBufferOffset += toCopy;
100
+ } else {
101
+ // Consumed entire front buffer, remove it
102
+ this._buffers.shift();
103
+ this._frontBufferOffset = 0;
104
+ }
105
+ }
106
+
107
+ this._size -= readAmount;
108
+ this._checkInvariants();
109
+ return readAmount;
110
+ }
111
+
112
+ /**
113
+ * Appends a buffer to the queue. Takes ownership of the buffer.
114
+ * Empty buffers are ignored.
115
+ *
116
+ * @param {Buffer} buffer - Buffer to append
117
+ * @throws {TypeError} If buffer is not a Buffer
118
+ */
119
+ append(buffer) {
120
+ if (!Buffer.isBuffer(buffer)) {
121
+ throw new TypeError('buffer must be a Buffer');
122
+ }
123
+
124
+ if (buffer.length === 0) {
125
+ return; // Ignore empty buffers
126
+ }
127
+
128
+ this._size += buffer.length;
129
+ this._buffers.push(buffer);
130
+ this._checkInvariants();
131
+ }
132
+
133
+ /**
134
+ * Clears all stored buffers.
135
+ */
136
+ clear() {
137
+ this._buffers = [];
138
+ this._frontBufferOffset = 0;
139
+ this._size = 0;
140
+ this._checkInvariants();
141
+ }
142
+
143
+ /**
144
+ * Reads and consumes exactly n bytes.
145
+ *
146
+ * @param {number} n - Number of bytes to read
147
+ * @returns {Buffer} Buffer containing exactly n bytes
148
+ * @throws {RangeError} If fewer than n bytes are available
149
+ */
150
+ read(n) {
151
+ if (n > this._size) {
152
+ throw new RangeError(`Cannot read ${n} bytes, only ${this._size} available`);
153
+ }
154
+ if (n === 0) {
155
+ return Buffer.allocUnsafe(0);
156
+ }
157
+
158
+ const result = Buffer.allocUnsafe(n);
159
+ const bytesRead = this.readInto(result);
160
+
161
+ if (bytesRead !== n) {
162
+ throw new Error(`Internal error: read ${bytesRead} bytes, expected ${n}`);
163
+ }
164
+
165
+ return result;
166
+ }
167
+
168
+ /**
169
+ * Peeks at data without consuming it.
170
+ *
171
+ * @param {number} [n=this._size] - Number of bytes to peek
172
+ * @returns {Buffer} Buffer containing up to n bytes (not consumed)
173
+ */
174
+ peek(n = this._size) {
175
+ const peekAmount = Math.min(n, this._size);
176
+ if (peekAmount === 0) {
177
+ return Buffer.allocUnsafe(0);
178
+ }
179
+
180
+ const result = Buffer.allocUnsafe(peekAmount);
181
+ let written = 0;
182
+ let bufferIndex = 0;
183
+ let offset = this._frontBufferOffset;
184
+
185
+ while (written < peekAmount && bufferIndex < this._buffers.length) {
186
+ const buffer = this._buffers[bufferIndex];
187
+ const available = buffer.length - offset;
188
+ const toCopy = Math.min(available, peekAmount - written);
189
+
190
+ buffer.copy(result, written, offset, offset + toCopy);
191
+ written += toCopy;
192
+
193
+ bufferIndex++;
194
+ offset = 0; // Reset offset for subsequent buffers
195
+ }
196
+
197
+ return result;
198
+ }
199
+
200
+ /**
201
+ * Checks internal invariants (development mode only).
202
+ * @private
203
+ * @throws {Error} If invariants are violated
204
+ */
205
+ _checkInvariants() {
206
+ if (process.env.NODE_ENV !== 'production') {
207
+ let bufferSizeSum = 0;
208
+ for (const buffer of this._buffers) {
209
+ if (buffer.length === 0) {
210
+ throw new Error('Invariant violation: empty buffer in queue');
211
+ }
212
+ bufferSizeSum += buffer.length;
213
+ }
214
+
215
+ const expectedSize = bufferSizeSum - this._frontBufferOffset;
216
+ if (this._size !== expectedSize) {
217
+ throw new Error(
218
+ `Invariant violation: size=${this._size}, expected=${expectedSize}`
219
+ );
220
+ }
221
+
222
+ if (this._buffers.length === 0) {
223
+ if (this._frontBufferOffset !== 0) {
224
+ throw new Error('Invariant violation: offset non-zero with empty queue');
225
+ }
226
+ } else {
227
+ if (this._frontBufferOffset >= this._buffers[0].length) {
228
+ throw new Error('Invariant violation: offset >= front buffer size');
229
+ }
230
+ }
231
+ }
232
+ }
233
+ }
234
+
235
+ module.exports = ByteBufferQueue;
@@ -0,0 +1,226 @@
1
+ /**
2
+ * @fileoverview RTCError - WebRTC-specific error types.
3
+ *
4
+ * Ported from Chromium's WebRTC implementation:
5
+ * chromium/src/third_party/blink/renderer/modules/peerconnection/rtc_error.{h,cc}
6
+ *
7
+ * Provides WebRTC-specific error types extending the standard Error class
8
+ * with additional error detail types and metadata fields.
9
+ *
10
+ * @license BSD-3-Clause
11
+ * @author nmhung1210
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ /**
17
+ * RTCErrorDetailType enum - Standardized WebRTC error details.
18
+ * Maps to RTCErrorDetailType from the WebRTC spec.
19
+ *
20
+ * @readonly
21
+ * @enum {string}
22
+ */
23
+ const RTCErrorDetailType = Object.freeze({
24
+ NONE: 'none',
25
+ DATA_CHANNEL_FAILURE: 'data-channel-failure',
26
+ DTLS_FAILURE: 'dtls-failure',
27
+ FINGERPRINT_FAILURE: 'fingerprint-failure',
28
+ SCTP_FAILURE: 'sctp-failure',
29
+ SDP_SYNTAX_ERROR: 'sdp-syntax-error',
30
+ HARDWARE_ENCODER_NOT_AVAILABLE: 'hardware-encoder-not-available',
31
+ HARDWARE_ENCODER_ERROR: 'hardware-encoder-error',
32
+ INVALID_STATE: 'invalid-state',
33
+ INVALID_MODIFICATION: 'invalid-modification',
34
+ INVALID_ACCESS_ERROR: 'invalid-access-error',
35
+ OPERATION_ERROR: 'operation-error'
36
+ });
37
+
38
+ /**
39
+ * RTCError extends Error with WebRTC-specific error details.
40
+ *
41
+ * @extends Error
42
+ */
43
+ class RTCError extends Error {
44
+ /**
45
+ * Creates a new RTCError.
46
+ *
47
+ * @param {RTCErrorInit} [init={}] - Error initialization dictionary
48
+ * @param {string} [init.errorDetail='none'] - Error detail type
49
+ * @param {number} [init.sdpLineNumber] - SDP line number where error occurred
50
+ * @param {number} [init.httpRequestStatusCode] - HTTP status code if relevant
51
+ * @param {number} [init.sctpCauseCode] - SCTP cause code
52
+ * @param {number} [init.receivedAlert] - TLS alert received
53
+ * @param {number} [init.sentAlert] - TLS alert sent
54
+ * @param {string} [message=''] - Error message
55
+ */
56
+ constructor(init = {}, message = '') {
57
+ super(message);
58
+
59
+ this.name = 'RTCError';
60
+
61
+ // Maintain stack trace in V8
62
+ if (Error.captureStackTrace) {
63
+ Error.captureStackTrace(this, RTCError);
64
+ }
65
+
66
+ // Validate and set errorDetail
67
+ const errorDetail = init.errorDetail || RTCErrorDetailType.NONE;
68
+ if (typeof errorDetail !== 'string') {
69
+ throw new TypeError('errorDetail must be a string');
70
+ }
71
+
72
+ /**
73
+ * Specific error category.
74
+ * @private {string}
75
+ */
76
+ this._errorDetail = errorDetail;
77
+
78
+ // Optional numeric fields with validation
79
+ this._sdpLineNumber = this._validateInteger(init.sdpLineNumber, 'sdpLineNumber');
80
+ this._httpRequestStatusCode = this._validateInteger(init.httpRequestStatusCode, 'httpRequestStatusCode');
81
+ this._sctpCauseCode = this._validateInteger(init.sctpCauseCode, 'sctpCauseCode');
82
+ this._receivedAlert = this._validateUnsignedInteger(init.receivedAlert, 'receivedAlert');
83
+ this._sentAlert = this._validateUnsignedInteger(init.sentAlert, 'sentAlert');
84
+ }
85
+
86
+ /**
87
+ * Validates that a value is an integer or null/undefined.
88
+ * @private
89
+ * @param {*} value - Value to validate
90
+ * @param {string} fieldName - Field name for error messages
91
+ * @returns {number|null}
92
+ * @throws {TypeError} If value is not an integer
93
+ */
94
+ _validateInteger(value, fieldName) {
95
+ if (value === undefined || value === null) {
96
+ return null;
97
+ }
98
+ const num = Number(value);
99
+ if (!Number.isInteger(num)) {
100
+ throw new TypeError(`${fieldName} must be an integer`);
101
+ }
102
+ return num;
103
+ }
104
+
105
+ /**
106
+ * Validates that a value is an unsigned integer or null/undefined.
107
+ * @private
108
+ * @param {*} value - Value to validate
109
+ * @param {string} fieldName - Field name for error messages
110
+ * @returns {number|null}
111
+ * @throws {TypeError} If value is not an unsigned integer
112
+ */
113
+ _validateUnsignedInteger(value, fieldName) {
114
+ if (value === undefined || value === null) {
115
+ return null;
116
+ }
117
+ const num = Number(value);
118
+ if (!Number.isInteger(num) || num < 0) {
119
+ throw new TypeError(`${fieldName} must be an unsigned integer`);
120
+ }
121
+ return num;
122
+ }
123
+
124
+ /**
125
+ * RTCErrorDetailType - specific error category.
126
+ * @type {string}
127
+ */
128
+ get errorDetail() {
129
+ return this._errorDetail;
130
+ }
131
+
132
+ /**
133
+ * SDP line number where the error occurred (if applicable).
134
+ * @type {number|null}
135
+ */
136
+ get sdpLineNumber() {
137
+ return this._sdpLineNumber;
138
+ }
139
+
140
+ /**
141
+ * HTTP request status code (if applicable).
142
+ * @type {number|null}
143
+ */
144
+ get httpRequestStatusCode() {
145
+ return this._httpRequestStatusCode;
146
+ }
147
+
148
+ /**
149
+ * SCTP cause code (if applicable).
150
+ * @type {number|null}
151
+ */
152
+ get sctpCauseCode() {
153
+ return this._sctpCauseCode;
154
+ }
155
+
156
+ /**
157
+ * TLS alert value received (if applicable).
158
+ * @type {number|null}
159
+ */
160
+ get receivedAlert() {
161
+ return this._receivedAlert;
162
+ }
163
+
164
+ /**
165
+ * TLS alert value sent (if applicable).
166
+ * @type {number|null}
167
+ */
168
+ get sentAlert() {
169
+ return this._sentAlert;
170
+ }
171
+
172
+ /**
173
+ * Converts error to JSON representation.
174
+ * @returns {Object} JSON representation of the error
175
+ */
176
+ toJSON() {
177
+ const json = {
178
+ name: this.name,
179
+ message: this.message,
180
+ errorDetail: this._errorDetail
181
+ };
182
+
183
+ if (this._sdpLineNumber !== null) {
184
+ json.sdpLineNumber = this._sdpLineNumber;
185
+ }
186
+ if (this._httpRequestStatusCode !== null) {
187
+ json.httpRequestStatusCode = this._httpRequestStatusCode;
188
+ }
189
+ if (this._sctpCauseCode !== null) {
190
+ json.sctpCauseCode = this._sctpCauseCode;
191
+ }
192
+ if (this._receivedAlert !== null) {
193
+ json.receivedAlert = this._receivedAlert;
194
+ }
195
+ if (this._sentAlert !== null) {
196
+ json.sentAlert = this._sentAlert;
197
+ }
198
+
199
+ return json;
200
+ }
201
+
202
+ /**
203
+ * Creates RTCError from a native WebRTC error object.
204
+ * @param {Object} nativeError - Native error object
205
+ * @param {string} [nativeError.error_detail] - Error detail type
206
+ * @param {number} [nativeError.sctp_cause_code] - SCTP cause code
207
+ * @param {string} [nativeError.message] - Error message
208
+ * @returns {RTCError}
209
+ */
210
+ static fromNative(nativeError) {
211
+ const init = {
212
+ errorDetail: nativeError.error_detail || RTCErrorDetailType.NONE
213
+ };
214
+
215
+ if (nativeError.sctp_cause_code !== undefined) {
216
+ init.sctpCauseCode = nativeError.sctp_cause_code;
217
+ }
218
+
219
+ return new RTCError(init, nativeError.message || 'Unknown error');
220
+ }
221
+ }
222
+
223
+ // Export error detail types as static property
224
+ RTCError.DetailType = RTCErrorDetailType;
225
+
226
+ module.exports = RTCError;