@libp2p/webrtc 2.0.11 → 3.0.0-72e81dc1
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 +7 -2
- package/dist/index.min.js +18 -19
- package/dist/src/error.d.ts +2 -2
- package/dist/src/error.d.ts.map +1 -1
- package/dist/src/error.js +1 -1
- package/dist/src/error.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/maconn.d.ts +5 -4
- package/dist/src/maconn.d.ts.map +1 -1
- package/dist/src/maconn.js +8 -5
- package/dist/src/maconn.js.map +1 -1
- package/dist/src/muxer.d.ts +10 -5
- package/dist/src/muxer.d.ts.map +1 -1
- package/dist/src/muxer.js +6 -2
- package/dist/src/muxer.js.map +1 -1
- package/dist/src/private-to-private/handler.d.ts +4 -3
- package/dist/src/private-to-private/handler.d.ts.map +1 -1
- package/dist/src/private-to-private/handler.js +104 -84
- package/dist/src/private-to-private/handler.js.map +1 -1
- package/dist/src/private-to-private/listener.d.ts +4 -3
- package/dist/src/private-to-private/listener.d.ts.map +1 -1
- package/dist/src/private-to-private/listener.js +1 -1
- package/dist/src/private-to-private/listener.js.map +1 -1
- package/dist/src/private-to-private/transport.d.ts +6 -5
- package/dist/src/private-to-private/transport.d.ts.map +1 -1
- package/dist/src/private-to-private/transport.js +13 -6
- package/dist/src/private-to-private/transport.js.map +1 -1
- package/dist/src/private-to-private/util.d.ts.map +1 -1
- package/dist/src/private-to-private/util.js +1 -0
- package/dist/src/private-to-private/util.js.map +1 -1
- package/dist/src/private-to-public/options.d.ts +1 -1
- package/dist/src/private-to-public/sdp.js +1 -1
- package/dist/src/private-to-public/transport.d.ts +4 -4
- package/dist/src/private-to-public/transport.d.ts.map +1 -1
- package/dist/src/private-to-public/transport.js +99 -92
- package/dist/src/private-to-public/transport.js.map +1 -1
- package/dist/src/private-to-public/util.d.ts.map +1 -1
- package/dist/src/private-to-public/util.js.map +1 -1
- package/dist/src/stream.d.ts +34 -3
- package/dist/src/stream.d.ts.map +1 -1
- package/dist/src/stream.js +24 -14
- package/dist/src/stream.js.map +1 -1
- package/dist/src/webrtc/index.browser.d.ts +15 -0
- package/dist/src/webrtc/index.browser.d.ts.map +1 -0
- package/dist/src/webrtc/index.browser.js +5 -0
- package/dist/src/webrtc/index.browser.js.map +1 -0
- package/dist/src/webrtc/index.d.ts +8 -0
- package/dist/src/webrtc/index.d.ts.map +1 -0
- package/dist/src/webrtc/index.js +11 -0
- package/dist/src/webrtc/index.js.map +1 -0
- package/dist/src/webrtc/rtc-data-channel.d.ts +29 -0
- package/dist/src/webrtc/rtc-data-channel.d.ts.map +1 -0
- package/dist/src/webrtc/rtc-data-channel.js +115 -0
- package/dist/src/webrtc/rtc-data-channel.js.map +1 -0
- package/dist/src/webrtc/rtc-events.d.ts +9 -0
- package/dist/src/webrtc/rtc-events.d.ts.map +1 -0
- package/dist/src/webrtc/rtc-events.js +15 -0
- package/dist/src/webrtc/rtc-events.js.map +1 -0
- package/dist/src/webrtc/rtc-ice-candidate.d.ts +22 -0
- package/dist/src/webrtc/rtc-ice-candidate.d.ts.map +1 -0
- package/dist/src/webrtc/rtc-ice-candidate.js +47 -0
- package/dist/src/webrtc/rtc-ice-candidate.js.map +1 -0
- package/dist/src/webrtc/rtc-peer-connection.d.ts +47 -0
- package/dist/src/webrtc/rtc-peer-connection.d.ts.map +1 -0
- package/dist/src/webrtc/rtc-peer-connection.js +245 -0
- package/dist/src/webrtc/rtc-peer-connection.js.map +1 -0
- package/dist/src/webrtc/rtc-session-description.d.ts +10 -0
- package/dist/src/webrtc/rtc-session-description.d.ts.map +1 -0
- package/dist/src/webrtc/rtc-session-description.js +18 -0
- package/dist/src/webrtc/rtc-session-description.js.map +1 -0
- package/package.json +29 -121
- package/src/error.ts +2 -2
- package/src/index.ts +1 -1
- package/src/maconn.ts +13 -8
- package/src/muxer.ts +11 -5
- package/src/private-to-private/handler.ts +124 -103
- package/src/private-to-private/listener.ts +4 -3
- package/src/private-to-private/transport.ts +20 -12
- package/src/private-to-private/util.ts +1 -0
- package/src/private-to-public/options.ts +1 -1
- package/src/private-to-public/sdp.ts +1 -1
- package/src/private-to-public/transport.ts +113 -107
- package/src/private-to-public/util.ts +0 -1
- package/src/stream.ts +29 -16
- package/src/webrtc/index.browser.ts +4 -0
- package/src/webrtc/index.ts +12 -0
- package/src/webrtc/rtc-data-channel.ts +140 -0
- package/src/webrtc/rtc-events.ts +19 -0
- package/src/webrtc/rtc-ice-candidate.ts +50 -0
- package/src/webrtc/rtc-peer-connection.ts +306 -0
- package/src/webrtc/rtc-session-description.ts +19 -0
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { CodeError } from '@libp2p/interface/errors'
|
|
2
|
+
import { type CreateListenerOptions, type DialOptions, symbol, type Transport, type Listener, type Upgrader } from '@libp2p/interface/transport'
|
|
3
3
|
import { logger } from '@libp2p/logger'
|
|
4
4
|
import { peerIdFromString } from '@libp2p/peer-id'
|
|
5
5
|
import { multiaddr, type Multiaddr, protocols } from '@multiformats/multiaddr'
|
|
6
6
|
import { codes } from '../error.js'
|
|
7
7
|
import { WebRTCMultiaddrConnection } from '../maconn.js'
|
|
8
|
+
import { cleanup } from '../webrtc/index.js'
|
|
8
9
|
import { initiateConnection, handleIncomingStream } from './handler.js'
|
|
9
10
|
import { WebRTCPeerListener } from './listener.js'
|
|
10
11
|
import type { DataChannelOpts } from '../stream.js'
|
|
11
|
-
import type { Connection } from '@libp2p/interface
|
|
12
|
-
import type { PeerId } from '@libp2p/interface
|
|
13
|
-
import type {
|
|
14
|
-
import type {
|
|
12
|
+
import type { Connection } from '@libp2p/interface/connection'
|
|
13
|
+
import type { PeerId } from '@libp2p/interface/peer-id'
|
|
14
|
+
import type { Startable } from '@libp2p/interface/startable'
|
|
15
|
+
import type { IncomingStreamData, Registrar } from '@libp2p/interface-internal/registrar'
|
|
16
|
+
import type { TransportManager } from '@libp2p/interface-internal/transport-manager'
|
|
15
17
|
|
|
16
18
|
const log = logger('libp2p:webrtc:peer')
|
|
17
19
|
|
|
@@ -48,12 +50,15 @@ export class WebRTCTransport implements Transport, Startable {
|
|
|
48
50
|
async start (): Promise<void> {
|
|
49
51
|
await this.components.registrar.handle(SIGNALING_PROTO_ID, (data: IncomingStreamData) => {
|
|
50
52
|
this._onProtocol(data).catch(err => { log.error('failed to handle incoming connect from %p', data.connection.remotePeer, err) })
|
|
53
|
+
}, {
|
|
54
|
+
runOnTransientConnection: true
|
|
51
55
|
})
|
|
52
56
|
this._started = true
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
async stop (): Promise<void> {
|
|
56
60
|
await this.components.registrar.unhandle(SIGNALING_PROTO_ID)
|
|
61
|
+
cleanup()
|
|
57
62
|
this._started = false
|
|
58
63
|
}
|
|
59
64
|
|
|
@@ -89,7 +94,10 @@ export class WebRTCTransport implements Transport, Startable {
|
|
|
89
94
|
}
|
|
90
95
|
|
|
91
96
|
const connection = await this.components.transportManager.dial(baseAddr, options)
|
|
92
|
-
const signalingStream = await connection.newStream(
|
|
97
|
+
const signalingStream = await connection.newStream(SIGNALING_PROTO_ID, {
|
|
98
|
+
...options,
|
|
99
|
+
runOnTransientConnection: true
|
|
100
|
+
})
|
|
93
101
|
|
|
94
102
|
try {
|
|
95
103
|
const { pc, muxerFactory, remoteAddress } = await initiateConnection({
|
|
@@ -113,11 +121,11 @@ export class WebRTCTransport implements Transport, Startable {
|
|
|
113
121
|
)
|
|
114
122
|
|
|
115
123
|
// close the stream if SDP has been exchanged successfully
|
|
116
|
-
signalingStream.close()
|
|
124
|
+
await signalingStream.close()
|
|
117
125
|
return result
|
|
118
|
-
} catch (err) {
|
|
126
|
+
} catch (err: any) {
|
|
119
127
|
// reset the stream in case of any error
|
|
120
|
-
signalingStream.
|
|
128
|
+
signalingStream.abort(err)
|
|
121
129
|
throw err
|
|
122
130
|
} finally {
|
|
123
131
|
// Close the signaling connection
|
|
@@ -143,8 +151,8 @@ export class WebRTCTransport implements Transport, Startable {
|
|
|
143
151
|
skipProtection: true,
|
|
144
152
|
muxerFactory
|
|
145
153
|
})
|
|
146
|
-
} catch (err) {
|
|
147
|
-
stream.
|
|
154
|
+
} catch (err: any) {
|
|
155
|
+
stream.abort(err)
|
|
148
156
|
throw err
|
|
149
157
|
} finally {
|
|
150
158
|
// Close the signaling connection
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CreateListenerOptions, DialOptions } from '@libp2p/interface
|
|
1
|
+
import type { CreateListenerOptions, DialOptions } from '@libp2p/interface/transport'
|
|
2
2
|
|
|
3
3
|
export interface WebRTCListenerOptions extends CreateListenerOptions {}
|
|
4
4
|
export interface WebRTCDialOptions extends DialOptions {}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { noise as Noise } from '@chainsafe/libp2p-noise'
|
|
2
|
-
import { type CreateListenerOptions,
|
|
2
|
+
import { type CreateListenerOptions, symbol, type Transport, type Listener } from '@libp2p/interface/transport'
|
|
3
3
|
import { logger } from '@libp2p/logger'
|
|
4
4
|
import * as p from '@libp2p/peer-id'
|
|
5
5
|
import { protocols } from '@multiformats/multiaddr'
|
|
@@ -11,13 +11,14 @@ import { WebRTCMultiaddrConnection } from '../maconn.js'
|
|
|
11
11
|
import { DataChannelMuxerFactory } from '../muxer.js'
|
|
12
12
|
import { createStream } from '../stream.js'
|
|
13
13
|
import { isFirefox } from '../util.js'
|
|
14
|
+
import { RTCPeerConnection } from '../webrtc/index.js'
|
|
14
15
|
import * as sdp from './sdp.js'
|
|
15
16
|
import { genUfrag } from './util.js'
|
|
16
17
|
import type { WebRTCDialOptions } from './options.js'
|
|
17
18
|
import type { DataChannelOpts } from '../stream.js'
|
|
18
|
-
import type { Connection } from '@libp2p/interface
|
|
19
|
-
import type { CounterGroup, Metrics } from '@libp2p/interface
|
|
20
|
-
import type { PeerId } from '@libp2p/interface
|
|
19
|
+
import type { Connection } from '@libp2p/interface/connection'
|
|
20
|
+
import type { CounterGroup, Metrics } from '@libp2p/interface/metrics'
|
|
21
|
+
import type { PeerId } from '@libp2p/interface/peer-id'
|
|
21
22
|
import type { Multiaddr } from '@multiformats/multiaddr'
|
|
22
23
|
|
|
23
24
|
const log = logger('libp2p:webrtc:transport')
|
|
@@ -134,116 +135,121 @@ export class WebRTCDirectTransport implements Transport {
|
|
|
134
135
|
|
|
135
136
|
const peerConnection = new RTCPeerConnection({ certificates: [certificate] })
|
|
136
137
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
// ref: https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/error_event
|
|
155
|
-
handshakeDataChannel.onerror = (event: Event) => {
|
|
156
|
-
clearTimeout(handshakeTimeout)
|
|
157
|
-
const errorTarget = event.target?.toString() ?? 'not specified'
|
|
158
|
-
const error = `Error opening a data channel for handshaking: ${errorTarget}`
|
|
159
|
-
log.error(error)
|
|
160
|
-
// NOTE: We use unknown error here but this could potentially be considered a reset by some standards.
|
|
161
|
-
this.metrics?.dialerEvents.increment({ unknown_error: true })
|
|
162
|
-
reject(dataChannelError('data', error))
|
|
163
|
-
}
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
const ufrag = 'libp2p+webrtc+v1/' + genUfrag(32)
|
|
167
|
-
|
|
168
|
-
// Create offer and munge sdp with ufrag == pwd. This allows the remote to
|
|
169
|
-
// respond to STUN messages without performing an actual SDP exchange.
|
|
170
|
-
// This is because it can infer the passwd field by reading the USERNAME
|
|
171
|
-
// attribute of the STUN message.
|
|
172
|
-
const offerSdp = await peerConnection.createOffer()
|
|
173
|
-
const mungedOfferSdp = sdp.munge(offerSdp, ufrag)
|
|
174
|
-
await peerConnection.setLocalDescription(mungedOfferSdp)
|
|
175
|
-
|
|
176
|
-
// construct answer sdp from multiaddr and ufrag
|
|
177
|
-
const answerSdp = sdp.fromMultiAddr(ma, ufrag)
|
|
178
|
-
await peerConnection.setRemoteDescription(answerSdp)
|
|
179
|
-
|
|
180
|
-
// wait for peerconnection.onopen to fire, or for the datachannel to open
|
|
181
|
-
const handshakeDataChannel = await dataChannelOpenPromise
|
|
182
|
-
|
|
183
|
-
const myPeerId = this.components.peerId
|
|
184
|
-
|
|
185
|
-
// Do noise handshake.
|
|
186
|
-
// Set the Noise Prologue to libp2p-webrtc-noise:<FINGERPRINTS> before starting the actual Noise handshake.
|
|
187
|
-
// <FINGERPRINTS> is the concatenation of the of the two TLS fingerprints of A and B in their multihash byte representation, sorted in ascending order.
|
|
188
|
-
const fingerprintsPrologue = this.generateNoisePrologue(peerConnection, remoteCerthash.code, ma)
|
|
189
|
-
|
|
190
|
-
// Since we use the default crypto interface and do not use a static key or early data,
|
|
191
|
-
// we pass in undefined for these parameters.
|
|
192
|
-
const noise = Noise({ prologueBytes: fingerprintsPrologue })()
|
|
193
|
-
|
|
194
|
-
const wrappedChannel = createStream({ channel: handshakeDataChannel, direction: 'inbound', dataChannelOptions: this.init.dataChannel })
|
|
195
|
-
const wrappedDuplex = {
|
|
196
|
-
...wrappedChannel,
|
|
197
|
-
sink: wrappedChannel.sink.bind(wrappedChannel),
|
|
198
|
-
source: (async function * () {
|
|
199
|
-
for await (const list of wrappedChannel.source) {
|
|
200
|
-
for (const buf of list) {
|
|
201
|
-
yield buf
|
|
202
|
-
}
|
|
138
|
+
try {
|
|
139
|
+
// create data channel for running the noise handshake. Once the data channel is opened,
|
|
140
|
+
// the remote will initiate the noise handshake. This is used to confirm the identity of
|
|
141
|
+
// the peer.
|
|
142
|
+
const dataChannelOpenPromise = new Promise<RTCDataChannel>((resolve, reject) => {
|
|
143
|
+
const handshakeDataChannel = peerConnection.createDataChannel('', { negotiated: true, id: 0 })
|
|
144
|
+
const handshakeTimeout = setTimeout(() => {
|
|
145
|
+
const error = `Data channel was never opened: state: ${handshakeDataChannel.readyState}`
|
|
146
|
+
log.error(error)
|
|
147
|
+
this.metrics?.dialerEvents.increment({ open_error: true })
|
|
148
|
+
reject(dataChannelError('data', error))
|
|
149
|
+
}, HANDSHAKE_TIMEOUT_MS)
|
|
150
|
+
|
|
151
|
+
handshakeDataChannel.onopen = (_) => {
|
|
152
|
+
clearTimeout(handshakeTimeout)
|
|
153
|
+
resolve(handshakeDataChannel)
|
|
203
154
|
}
|
|
204
|
-
}())
|
|
205
|
-
}
|
|
206
155
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
156
|
+
// ref: https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/error_event
|
|
157
|
+
handshakeDataChannel.onerror = (event: Event) => {
|
|
158
|
+
clearTimeout(handshakeTimeout)
|
|
159
|
+
const errorTarget = event.target?.toString() ?? 'not specified'
|
|
160
|
+
const error = `Error opening a data channel for handshaking: ${errorTarget}`
|
|
161
|
+
log.error(error)
|
|
162
|
+
// NOTE: We use unknown error here but this could potentially be considered a reset by some standards.
|
|
163
|
+
this.metrics?.dialerEvents.increment({ unknown_error: true })
|
|
164
|
+
reject(dataChannelError('data', error))
|
|
165
|
+
}
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
const ufrag = 'libp2p+webrtc+v1/' + genUfrag(32)
|
|
169
|
+
|
|
170
|
+
// Create offer and munge sdp with ufrag == pwd. This allows the remote to
|
|
171
|
+
// respond to STUN messages without performing an actual SDP exchange.
|
|
172
|
+
// This is because it can infer the passwd field by reading the USERNAME
|
|
173
|
+
// attribute of the STUN message.
|
|
174
|
+
const offerSdp = await peerConnection.createOffer()
|
|
175
|
+
const mungedOfferSdp = sdp.munge(offerSdp, ufrag)
|
|
176
|
+
await peerConnection.setLocalDescription(mungedOfferSdp)
|
|
177
|
+
|
|
178
|
+
// construct answer sdp from multiaddr and ufrag
|
|
179
|
+
const answerSdp = sdp.fromMultiAddr(ma, ufrag)
|
|
180
|
+
await peerConnection.setRemoteDescription(answerSdp)
|
|
181
|
+
|
|
182
|
+
// wait for peerconnection.onopen to fire, or for the datachannel to open
|
|
183
|
+
const handshakeDataChannel = await dataChannelOpenPromise
|
|
184
|
+
|
|
185
|
+
const myPeerId = this.components.peerId
|
|
186
|
+
|
|
187
|
+
// Do noise handshake.
|
|
188
|
+
// Set the Noise Prologue to libp2p-webrtc-noise:<FINGERPRINTS> before starting the actual Noise handshake.
|
|
189
|
+
// <FINGERPRINTS> is the concatenation of the of the two TLS fingerprints of A and B in their multihash byte representation, sorted in ascending order.
|
|
190
|
+
const fingerprintsPrologue = this.generateNoisePrologue(peerConnection, remoteCerthash.code, ma)
|
|
191
|
+
|
|
192
|
+
// Since we use the default crypto interface and do not use a static key or early data,
|
|
193
|
+
// we pass in undefined for these parameters.
|
|
194
|
+
const noise = Noise({ prologueBytes: fingerprintsPrologue })()
|
|
195
|
+
|
|
196
|
+
const wrappedChannel = createStream({ channel: handshakeDataChannel, direction: 'inbound', dataChannelOptions: this.init.dataChannel })
|
|
197
|
+
const wrappedDuplex = {
|
|
198
|
+
...wrappedChannel,
|
|
199
|
+
sink: wrappedChannel.sink.bind(wrappedChannel),
|
|
200
|
+
source: (async function * () {
|
|
201
|
+
for await (const list of wrappedChannel.source) {
|
|
202
|
+
for (const buf of list) {
|
|
203
|
+
yield buf
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}())
|
|
234
207
|
}
|
|
235
|
-
}, { signal })
|
|
236
208
|
|
|
237
|
-
|
|
238
|
-
|
|
209
|
+
// Creating the connection before completion of the noise
|
|
210
|
+
// handshake ensures that the stream opening callback is set up
|
|
211
|
+
const maConn = new WebRTCMultiaddrConnection({
|
|
212
|
+
peerConnection,
|
|
213
|
+
remoteAddr: ma,
|
|
214
|
+
timeline: {
|
|
215
|
+
open: Date.now()
|
|
216
|
+
},
|
|
217
|
+
metrics: this.metrics?.dialerEvents
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
const eventListeningName = isFirefox ? 'iceconnectionstatechange' : 'connectionstatechange'
|
|
221
|
+
|
|
222
|
+
peerConnection.addEventListener(eventListeningName, () => {
|
|
223
|
+
switch (peerConnection.connectionState) {
|
|
224
|
+
case 'failed':
|
|
225
|
+
case 'disconnected':
|
|
226
|
+
case 'closed':
|
|
227
|
+
maConn.close().catch((err) => {
|
|
228
|
+
log.error('error closing connection', err)
|
|
229
|
+
}).finally(() => {
|
|
230
|
+
// Remove the event listener once the connection is closed
|
|
231
|
+
controller.abort()
|
|
232
|
+
})
|
|
233
|
+
break
|
|
234
|
+
default:
|
|
235
|
+
break
|
|
236
|
+
}
|
|
237
|
+
}, { signal })
|
|
239
238
|
|
|
240
|
-
|
|
239
|
+
// Track opened peer connection
|
|
240
|
+
this.metrics?.dialerEvents.increment({ peer_connection: true })
|
|
241
241
|
|
|
242
|
-
|
|
243
|
-
// Therefore, we need to secure an inbound noise connection from the remote.
|
|
244
|
-
await noise.secureInbound(myPeerId, wrappedDuplex, theirPeerId)
|
|
242
|
+
const muxerFactory = new DataChannelMuxerFactory({ peerConnection, metrics: this.metrics?.dialerEvents, dataChannelOptions: this.init.dataChannel })
|
|
245
243
|
|
|
246
|
-
|
|
244
|
+
// For outbound connections, the remote is expected to start the noise handshake.
|
|
245
|
+
// Therefore, we need to secure an inbound noise connection from the remote.
|
|
246
|
+
await noise.secureInbound(myPeerId, wrappedDuplex, theirPeerId)
|
|
247
|
+
|
|
248
|
+
return await options.upgrader.upgradeOutbound(maConn, { skipProtection: true, skipEncryption: true, muxerFactory })
|
|
249
|
+
} catch (err) {
|
|
250
|
+
peerConnection.close()
|
|
251
|
+
throw err
|
|
252
|
+
}
|
|
247
253
|
}
|
|
248
254
|
|
|
249
255
|
/**
|
package/src/stream.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { CodeError } from '@libp2p/interface/errors'
|
|
2
|
+
import { AbstractStream, type AbstractStreamInit } from '@libp2p/interface/stream-muxer/stream'
|
|
3
3
|
import { logger } from '@libp2p/logger'
|
|
4
4
|
import * as lengthPrefixed from 'it-length-prefixed'
|
|
5
5
|
import { type Pushable, pushable } from 'it-pushable'
|
|
6
6
|
import { pEvent, TimeoutError } from 'p-event'
|
|
7
7
|
import { Uint8ArrayList } from 'uint8arraylist'
|
|
8
8
|
import { Message } from './pb/message.js'
|
|
9
|
-
import type { Direction
|
|
9
|
+
import type { Direction } from '@libp2p/interface/connection'
|
|
10
10
|
|
|
11
11
|
const log = logger('libp2p:webrtc:stream')
|
|
12
12
|
|
|
@@ -26,6 +26,8 @@ export interface WebRTCStreamInit extends AbstractStreamInit {
|
|
|
26
26
|
channel: RTCDataChannel
|
|
27
27
|
|
|
28
28
|
dataChannelOptions?: Partial<DataChannelOpts>
|
|
29
|
+
|
|
30
|
+
maxDataSize: number
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
// Max message size that can be sent to the DataChannel
|
|
@@ -40,7 +42,7 @@ const BUFFERED_AMOUNT_LOW_TIMEOUT = 30 * 1000
|
|
|
40
42
|
// protobuf field definition overhead
|
|
41
43
|
const PROTOBUF_OVERHEAD = 3
|
|
42
44
|
|
|
43
|
-
class WebRTCStream extends AbstractStream {
|
|
45
|
+
export class WebRTCStream extends AbstractStream {
|
|
44
46
|
/**
|
|
45
47
|
* The data channel used to send and receive data
|
|
46
48
|
*/
|
|
@@ -58,6 +60,7 @@ class WebRTCStream extends AbstractStream {
|
|
|
58
60
|
private readonly incomingData: Pushable<Uint8Array>
|
|
59
61
|
|
|
60
62
|
private messageQueue?: Uint8ArrayList
|
|
63
|
+
private readonly maxDataSize: number
|
|
61
64
|
|
|
62
65
|
constructor (init: WebRTCStreamInit) {
|
|
63
66
|
super(init)
|
|
@@ -71,6 +74,7 @@ class WebRTCStream extends AbstractStream {
|
|
|
71
74
|
maxBufferedAmount: init.dataChannelOptions?.maxBufferedAmount ?? MAX_BUFFERED_AMOUNT,
|
|
72
75
|
maxMessageSize: init.dataChannelOptions?.maxMessageSize ?? MAX_MESSAGE_SIZE
|
|
73
76
|
}
|
|
77
|
+
this.maxDataSize = init.maxDataSize
|
|
74
78
|
|
|
75
79
|
// set up initial state
|
|
76
80
|
switch (this.channel.readyState) {
|
|
@@ -79,8 +83,8 @@ class WebRTCStream extends AbstractStream {
|
|
|
79
83
|
|
|
80
84
|
case 'closed':
|
|
81
85
|
case 'closing':
|
|
82
|
-
if (this.
|
|
83
|
-
this.
|
|
86
|
+
if (this.timeline.close === undefined || this.timeline.close === 0) {
|
|
87
|
+
this.timeline.close = Date.now()
|
|
84
88
|
}
|
|
85
89
|
break
|
|
86
90
|
case 'connecting':
|
|
@@ -94,7 +98,7 @@ class WebRTCStream extends AbstractStream {
|
|
|
94
98
|
|
|
95
99
|
// handle RTCDataChannel events
|
|
96
100
|
this.channel.onopen = (_evt) => {
|
|
97
|
-
this.
|
|
101
|
+
this.timeline.open = new Date().getTime()
|
|
98
102
|
|
|
99
103
|
if (this.messageQueue != null) {
|
|
100
104
|
// send any queued messages
|
|
@@ -107,7 +111,9 @@ class WebRTCStream extends AbstractStream {
|
|
|
107
111
|
}
|
|
108
112
|
|
|
109
113
|
this.channel.onclose = (_evt) => {
|
|
110
|
-
this.close()
|
|
114
|
+
void this.close().catch(err => {
|
|
115
|
+
log.error('error closing stream after channel closed', err)
|
|
116
|
+
})
|
|
111
117
|
}
|
|
112
118
|
|
|
113
119
|
this.channel.onerror = (evt) => {
|
|
@@ -153,7 +159,6 @@ class WebRTCStream extends AbstractStream {
|
|
|
153
159
|
await pEvent(this.channel, 'bufferedamountlow', { timeout: this.dataChannelOptions.bufferedAmountLowEventTimeout })
|
|
154
160
|
} catch (err: any) {
|
|
155
161
|
if (err instanceof TimeoutError) {
|
|
156
|
-
this.abort(err)
|
|
157
162
|
throw new Error('Timed out waiting for DataChannel buffer to clear')
|
|
158
163
|
}
|
|
159
164
|
|
|
@@ -184,10 +189,17 @@ class WebRTCStream extends AbstractStream {
|
|
|
184
189
|
}
|
|
185
190
|
|
|
186
191
|
async sendData (data: Uint8ArrayList): Promise<void> {
|
|
187
|
-
|
|
188
|
-
const sendbuf = lengthPrefixed.encode.single(msgbuf)
|
|
192
|
+
data = data.sublist()
|
|
189
193
|
|
|
190
|
-
|
|
194
|
+
while (data.byteLength > 0) {
|
|
195
|
+
const toSend = Math.min(data.byteLength, this.maxDataSize)
|
|
196
|
+
const buf = data.subarray(0, toSend)
|
|
197
|
+
const msgbuf = Message.encode({ message: buf })
|
|
198
|
+
const sendbuf = lengthPrefixed.encode.single(msgbuf)
|
|
199
|
+
await this._sendMessage(sendbuf)
|
|
200
|
+
|
|
201
|
+
data.consume(toSend)
|
|
202
|
+
}
|
|
191
203
|
}
|
|
192
204
|
|
|
193
205
|
async sendReset (): Promise<void> {
|
|
@@ -212,7 +224,7 @@ class WebRTCStream extends AbstractStream {
|
|
|
212
224
|
if (message.flag === Message.Flag.FIN) {
|
|
213
225
|
// We should expect no more data from the remote, stop reading
|
|
214
226
|
this.incomingData.end()
|
|
215
|
-
this.
|
|
227
|
+
this.remoteCloseWrite()
|
|
216
228
|
}
|
|
217
229
|
|
|
218
230
|
if (message.flag === Message.Flag.RESET) {
|
|
@@ -222,7 +234,7 @@ class WebRTCStream extends AbstractStream {
|
|
|
222
234
|
|
|
223
235
|
if (message.flag === Message.Flag.STOP_SENDING) {
|
|
224
236
|
// The remote has stopped reading
|
|
225
|
-
this.
|
|
237
|
+
this.remoteCloseRead()
|
|
226
238
|
}
|
|
227
239
|
}
|
|
228
240
|
|
|
@@ -259,7 +271,7 @@ export interface WebRTCStreamOptions {
|
|
|
259
271
|
onEnd?: (err?: Error | undefined) => void
|
|
260
272
|
}
|
|
261
273
|
|
|
262
|
-
export function createStream (options: WebRTCStreamOptions):
|
|
274
|
+
export function createStream (options: WebRTCStreamOptions): WebRTCStream {
|
|
263
275
|
const { channel, direction, onEnd, dataChannelOptions } = options
|
|
264
276
|
|
|
265
277
|
return new WebRTCStream({
|
|
@@ -268,6 +280,7 @@ export function createStream (options: WebRTCStreamOptions): Stream {
|
|
|
268
280
|
maxDataSize: (dataChannelOptions?.maxMessageSize ?? MAX_MESSAGE_SIZE) - PROTOBUF_OVERHEAD,
|
|
269
281
|
dataChannelOptions,
|
|
270
282
|
onEnd,
|
|
271
|
-
channel
|
|
283
|
+
channel,
|
|
284
|
+
log: logger(`libp2p:mplex:stream:${direction}:${channel.id}`)
|
|
272
285
|
})
|
|
273
286
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import node from 'node-datachannel'
|
|
2
|
+
import { IceCandidate } from './rtc-ice-candidate.js'
|
|
3
|
+
import { PeerConnection } from './rtc-peer-connection.js'
|
|
4
|
+
import { SessionDescription } from './rtc-session-description.js'
|
|
5
|
+
|
|
6
|
+
export { SessionDescription as RTCSessionDescription }
|
|
7
|
+
export { IceCandidate as RTCIceCandidate }
|
|
8
|
+
export { PeerConnection as RTCPeerConnection }
|
|
9
|
+
|
|
10
|
+
export function cleanup (): void {
|
|
11
|
+
node.cleanup()
|
|
12
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
|
|
2
|
+
import type node from 'node-datachannel'
|
|
3
|
+
|
|
4
|
+
export class DataChannel extends EventTarget implements RTCDataChannel {
|
|
5
|
+
binaryType: BinaryType
|
|
6
|
+
|
|
7
|
+
readonly maxPacketLifeTime: number | null
|
|
8
|
+
readonly maxRetransmits: number | null
|
|
9
|
+
readonly negotiated: boolean
|
|
10
|
+
readonly ordered: boolean
|
|
11
|
+
|
|
12
|
+
onbufferedamountlow: ((this: RTCDataChannel, ev: Event) => any) | null
|
|
13
|
+
onclose: ((this: RTCDataChannel, ev: Event) => any) | null
|
|
14
|
+
onclosing: ((this: RTCDataChannel, ev: Event) => any) | null
|
|
15
|
+
onerror: ((this: RTCDataChannel, ev: Event) => any) | null
|
|
16
|
+
onmessage: ((this: RTCDataChannel, ev: MessageEvent) => any) | null
|
|
17
|
+
onopen: ((this: RTCDataChannel, ev: Event) => any) | null
|
|
18
|
+
|
|
19
|
+
#dataChannel: node.DataChannel
|
|
20
|
+
#bufferedAmountLowThreshold: number
|
|
21
|
+
#readyState: RTCDataChannelState
|
|
22
|
+
|
|
23
|
+
constructor (dataChannel: node.DataChannel, dataChannelDict: RTCDataChannelInit = {}) {
|
|
24
|
+
super()
|
|
25
|
+
|
|
26
|
+
this.#dataChannel = dataChannel
|
|
27
|
+
this.#readyState = 'connecting'
|
|
28
|
+
this.#bufferedAmountLowThreshold = 0
|
|
29
|
+
|
|
30
|
+
this.binaryType = 'arraybuffer'
|
|
31
|
+
|
|
32
|
+
this.#dataChannel.onOpen(() => {
|
|
33
|
+
this.#readyState = 'open'
|
|
34
|
+
this.dispatchEvent(new Event('open'))
|
|
35
|
+
})
|
|
36
|
+
this.#dataChannel.onClosed(() => {
|
|
37
|
+
this.#readyState = 'closed'
|
|
38
|
+
this.dispatchEvent(new Event('close'))
|
|
39
|
+
})
|
|
40
|
+
this.#dataChannel.onError((msg) => {
|
|
41
|
+
this.#readyState = 'closed'
|
|
42
|
+
this.dispatchEvent(new RTCErrorEvent('error', {
|
|
43
|
+
error: new RTCError({
|
|
44
|
+
errorDetail: 'data-channel-failure'
|
|
45
|
+
}, msg)
|
|
46
|
+
}))
|
|
47
|
+
})
|
|
48
|
+
this.#dataChannel.onBufferedAmountLow(() => {
|
|
49
|
+
this.dispatchEvent(new Event('bufferedamountlow'))
|
|
50
|
+
})
|
|
51
|
+
this.#dataChannel.onMessage((data: string | Uint8Array) => {
|
|
52
|
+
if (typeof data === 'string') {
|
|
53
|
+
data = uint8ArrayFromString(data)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
this.dispatchEvent(new MessageEvent('message', { data }))
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// forward events to properties
|
|
60
|
+
this.addEventListener('message', event => {
|
|
61
|
+
this.onmessage?.(event as MessageEvent<ArrayBuffer>)
|
|
62
|
+
})
|
|
63
|
+
this.addEventListener('bufferedamountlow', event => {
|
|
64
|
+
this.onbufferedamountlow?.(event)
|
|
65
|
+
})
|
|
66
|
+
this.addEventListener('error', event => {
|
|
67
|
+
this.onerror?.(event)
|
|
68
|
+
})
|
|
69
|
+
this.addEventListener('close', event => {
|
|
70
|
+
this.onclose?.(event)
|
|
71
|
+
})
|
|
72
|
+
this.addEventListener('closing', event => {
|
|
73
|
+
this.onclosing?.(event)
|
|
74
|
+
})
|
|
75
|
+
this.addEventListener('open', event => {
|
|
76
|
+
this.onopen?.(event)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
this.onbufferedamountlow = null
|
|
80
|
+
this.onclose = null
|
|
81
|
+
this.onclosing = null
|
|
82
|
+
this.onerror = null
|
|
83
|
+
this.onmessage = null
|
|
84
|
+
this.onopen = null
|
|
85
|
+
|
|
86
|
+
this.maxPacketLifeTime = dataChannelDict.maxPacketLifeTime ?? null
|
|
87
|
+
this.maxRetransmits = dataChannelDict.maxRetransmits ?? null
|
|
88
|
+
this.negotiated = dataChannelDict.negotiated ?? false
|
|
89
|
+
this.ordered = dataChannelDict.ordered ?? true
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
get id (): number {
|
|
93
|
+
return this.#dataChannel.getId()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
get label (): string {
|
|
97
|
+
return this.#dataChannel.getLabel()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
get protocol (): string {
|
|
101
|
+
return this.#dataChannel.getProtocol()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
get bufferedAmount (): number {
|
|
105
|
+
return this.#dataChannel.bufferedAmount()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
set bufferedAmountLowThreshold (threshold: number) {
|
|
109
|
+
this.#bufferedAmountLowThreshold = threshold
|
|
110
|
+
this.#dataChannel.setBufferedAmountLowThreshold(threshold)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
get bufferedAmountLowThreshold (): number {
|
|
114
|
+
return this.#bufferedAmountLowThreshold
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
get readyState (): RTCDataChannelState {
|
|
118
|
+
return this.#readyState
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
close (): void {
|
|
122
|
+
this.#readyState = 'closing'
|
|
123
|
+
this.dispatchEvent(new Event('closing'))
|
|
124
|
+
|
|
125
|
+
this.#dataChannel.close()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
send (data: string): void
|
|
129
|
+
send (data: Blob): void
|
|
130
|
+
send (data: ArrayBuffer): void
|
|
131
|
+
send (data: ArrayBufferView): void
|
|
132
|
+
send (data: any): void {
|
|
133
|
+
// TODO: sending Blobs
|
|
134
|
+
if (typeof data === 'string') {
|
|
135
|
+
this.#dataChannel.sendMessage(data)
|
|
136
|
+
} else {
|
|
137
|
+
this.#dataChannel.sendMessageBinary(data)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|