@libp2p/webrtc 0.0.0
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/LICENSE +4 -0
- package/README.md +210 -0
- package/dist/index.min.js +54 -0
- package/dist/index.min.js.map +7 -0
- package/dist/proto_ts/message.d.ts +56 -0
- package/dist/proto_ts/message.d.ts.map +1 -0
- package/dist/proto_ts/message.js +86 -0
- package/dist/proto_ts/message.js.map +1 -0
- package/dist/src/error.d.ts +54 -0
- package/dist/src/error.d.ts.map +1 -0
- package/dist/src/error.js +105 -0
- package/dist/src/error.js.map +1 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +6 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/maconn.d.ts +43 -0
- package/dist/src/maconn.d.ts.map +1 -0
- package/dist/src/maconn.js +29 -0
- package/dist/src/maconn.js.map +1 -0
- package/dist/src/muxer.d.ts +55 -0
- package/dist/src/muxer.d.ts.map +1 -0
- package/dist/src/muxer.js +92 -0
- package/dist/src/muxer.js.map +1 -0
- package/dist/src/options.d.ts +6 -0
- package/dist/src/options.d.ts.map +1 -0
- package/dist/src/options.js +2 -0
- package/dist/src/options.js.map +1 -0
- package/dist/src/sdp.d.ts +33 -0
- package/dist/src/sdp.d.ts.map +1 -0
- package/dist/src/sdp.js +118 -0
- package/dist/src/sdp.js.map +1 -0
- package/dist/src/stream.d.ts +141 -0
- package/dist/src/stream.d.ts.map +1 -0
- package/dist/src/stream.js +290 -0
- package/dist/src/stream.js.map +1 -0
- package/dist/src/transport.d.ts +60 -0
- package/dist/src/transport.d.ts.map +1 -0
- package/dist/src/transport.js +193 -0
- package/dist/src/transport.js.map +1 -0
- package/dist/src/util.d.ts +5 -0
- package/dist/src/util.d.ts.map +1 -0
- package/dist/src/util.js +5 -0
- package/dist/src/util.js.map +1 -0
- package/dist/stats.json +1 -0
- package/package.json +170 -0
- package/proto_ts/message.ts +105 -0
- package/src/error.ts +123 -0
- package/src/index.ts +6 -0
- package/src/maconn.ts +67 -0
- package/src/message.proto +22 -0
- package/src/muxer.ts +120 -0
- package/src/options.ts +4 -0
- package/src/sdp.ts +136 -0
- package/src/stream.ts +422 -0
- package/src/transport.ts +243 -0
- package/src/util.ts +5 -0
package/src/transport.ts
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { noise as Noise } from '@chainsafe/libp2p-noise'
|
|
2
|
+
import { Connection } from '@libp2p/interface-connection'
|
|
3
|
+
import type { PeerId } from '@libp2p/interface-peer-id'
|
|
4
|
+
import { CreateListenerOptions, Listener, symbol, Transport } from '@libp2p/interface-transport'
|
|
5
|
+
import { logger } from '@libp2p/logger'
|
|
6
|
+
import * as p from '@libp2p/peer-id'
|
|
7
|
+
import { Multiaddr } from '@multiformats/multiaddr'
|
|
8
|
+
import * as multihashes from 'multihashes'
|
|
9
|
+
import defer from 'p-defer'
|
|
10
|
+
import { v4 as genUuid } from 'uuid'
|
|
11
|
+
import { fromString as uint8arrayFromString } from 'uint8arrays/from-string'
|
|
12
|
+
import { concat } from 'uint8arrays/concat'
|
|
13
|
+
|
|
14
|
+
import { dataChannelError, inappropriateMultiaddr, unimplemented, invalidArgument } from './error.js'
|
|
15
|
+
import { WebRTCMultiaddrConnection } from './maconn.js'
|
|
16
|
+
import { DataChannelMuxerFactory } from './muxer.js'
|
|
17
|
+
import { WebRTCDialOptions } from './options.js'
|
|
18
|
+
import * as sdp from './sdp.js'
|
|
19
|
+
import { WebRTCStream } from './stream.js'
|
|
20
|
+
|
|
21
|
+
const log = logger('libp2p:webrtc:transport')
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The time to wait, in milliseconds, for the data channel handshake to complete
|
|
25
|
+
*/
|
|
26
|
+
const HANDSHAKE_TIMEOUT_MS = 10000
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Created by converting the hexadecimal protocol code to an integer.
|
|
30
|
+
*
|
|
31
|
+
* {@link https://github.com/multiformats/multiaddr/blob/master/protocols.csv}
|
|
32
|
+
*/
|
|
33
|
+
export const WEBRTC_CODE: number = 280
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Created by converting the hexadecimal protocol code to an integer.
|
|
37
|
+
*
|
|
38
|
+
* {@link https://github.com/multiformats/multiaddr/blob/master/protocols.csv}
|
|
39
|
+
*/
|
|
40
|
+
export const CERTHASH_CODE: number = 466
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The peer for this transport
|
|
44
|
+
*/
|
|
45
|
+
// @TODO(ddimaria): seems like an unnessary abstraction, consider removing
|
|
46
|
+
export interface WebRTCTransportComponents {
|
|
47
|
+
peerId: PeerId
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class WebRTCTransport implements Transport {
|
|
51
|
+
/**
|
|
52
|
+
* The peer for this transport
|
|
53
|
+
*/
|
|
54
|
+
private readonly components: WebRTCTransportComponents
|
|
55
|
+
|
|
56
|
+
constructor (components: WebRTCTransportComponents) {
|
|
57
|
+
this.components = components
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Dial a given multiaddr
|
|
62
|
+
*/
|
|
63
|
+
async dial (ma: Multiaddr, options: WebRTCDialOptions): Promise<Connection> {
|
|
64
|
+
const rawConn = await this._connect(ma, options)
|
|
65
|
+
log(`dialing address - ${ma.toString()}`)
|
|
66
|
+
return rawConn
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Create transport listeners no supported by browsers
|
|
71
|
+
*/
|
|
72
|
+
createListener (options: CreateListenerOptions): Listener {
|
|
73
|
+
throw unimplemented('WebRTCTransport.createListener')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Takes a list of `Multiaddr`s and returns only valid addresses for the transport
|
|
78
|
+
*/
|
|
79
|
+
filter (multiaddrs: Multiaddr[]): Multiaddr[] {
|
|
80
|
+
return multiaddrs.filter(validMa)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Implement toString() for WebRTCTransport
|
|
85
|
+
*/
|
|
86
|
+
get [Symbol.toStringTag] (): string {
|
|
87
|
+
return '@libp2p/webrtc'
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Symbol.for('@libp2p/transport')
|
|
92
|
+
*/
|
|
93
|
+
get [symbol] (): true {
|
|
94
|
+
return true
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Connect to a peer using a multiaddr
|
|
99
|
+
*/
|
|
100
|
+
async _connect (ma: Multiaddr, options: WebRTCDialOptions): Promise<Connection> {
|
|
101
|
+
const rps = ma.getPeerId()
|
|
102
|
+
|
|
103
|
+
if (rps === null) {
|
|
104
|
+
throw inappropriateMultiaddr("we need to have the remote's PeerId")
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const remoteCerthash = sdp.decodeCerthash(sdp.certhash(ma))
|
|
108
|
+
|
|
109
|
+
// ECDSA is preferred over RSA here. From our testing we find that P-256 elliptic
|
|
110
|
+
// curve is supported by Pion, webrtc-rs, as well as Chromium (P-228 and P-384
|
|
111
|
+
// was not supported in Chromium). We use the same hash function as found in the
|
|
112
|
+
// multiaddr if it is supported.
|
|
113
|
+
const certificate = await RTCPeerConnection.generateCertificate({
|
|
114
|
+
name: 'ECDSA',
|
|
115
|
+
namedCurve: 'P-256',
|
|
116
|
+
hash: sdp.toSupportedHashFunction(remoteCerthash.name)
|
|
117
|
+
} as any)
|
|
118
|
+
const peerConnection = new RTCPeerConnection({ certificates: [certificate] })
|
|
119
|
+
|
|
120
|
+
// create data channel for running the noise handshake. Once the data channel is opened,
|
|
121
|
+
// the remote will initiate the noise handshake. This is used to confirm the identity of
|
|
122
|
+
// the peer.
|
|
123
|
+
const dataChannelOpenPromise = defer()
|
|
124
|
+
const handshakeDataChannel = peerConnection.createDataChannel('handshake', { negotiated: true, id: 0 })
|
|
125
|
+
const handhsakeTimeout = setTimeout(() => {
|
|
126
|
+
const error = `Data channel was never opened: state: ${handshakeDataChannel.readyState}`
|
|
127
|
+
log.error(error)
|
|
128
|
+
dataChannelOpenPromise.reject(dataChannelError('data', error))
|
|
129
|
+
}, HANDSHAKE_TIMEOUT_MS)
|
|
130
|
+
|
|
131
|
+
handshakeDataChannel.onopen = (_) => {
|
|
132
|
+
clearTimeout(handhsakeTimeout)
|
|
133
|
+
dataChannelOpenPromise.resolve()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ref: https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/error_event
|
|
137
|
+
handshakeDataChannel.onerror = (event: Event) => {
|
|
138
|
+
clearTimeout(handhsakeTimeout)
|
|
139
|
+
const errorTarget = event.target?.toString() ?? 'not specified'
|
|
140
|
+
const error = `Error opening a data channel for handshaking: ${errorTarget}`
|
|
141
|
+
log.error(error)
|
|
142
|
+
dataChannelOpenPromise.reject(dataChannelError('data', error))
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const ufrag = 'libp2p+webrtc+v1/' + genUuid().replaceAll('-', '')
|
|
146
|
+
|
|
147
|
+
// Create offer and munge sdp with ufrag = pwd. This allows the remote to
|
|
148
|
+
// respond to STUN messages without performing an actual SDP exchange.
|
|
149
|
+
// This is because it can infer the passwd field by reading the USERNAME
|
|
150
|
+
// attribute of the STUN message.
|
|
151
|
+
const offerSdp = await peerConnection.createOffer()
|
|
152
|
+
const mungedOfferSdp = sdp.munge(offerSdp, ufrag)
|
|
153
|
+
await peerConnection.setLocalDescription(mungedOfferSdp)
|
|
154
|
+
|
|
155
|
+
// construct answer sdp from multiaddr and ufrag
|
|
156
|
+
const answerSdp = sdp.fromMultiAddr(ma, ufrag)
|
|
157
|
+
await peerConnection.setRemoteDescription(answerSdp)
|
|
158
|
+
|
|
159
|
+
// wait for peerconnection.onopen to fire, or for the datachannel to open
|
|
160
|
+
await dataChannelOpenPromise.promise
|
|
161
|
+
|
|
162
|
+
const myPeerId = this.components.peerId
|
|
163
|
+
const theirPeerId = p.peerIdFromString(rps)
|
|
164
|
+
|
|
165
|
+
// Do noise handshake.
|
|
166
|
+
// Set the Noise Prologue to libp2p-webrtc-noise:<FINGERPRINTS> before starting the actual Noise handshake.
|
|
167
|
+
// <FINGERPRINTS> is the concatenation of the of the two TLS fingerprints of A and B in their multihash byte representation, sorted in ascending order.
|
|
168
|
+
const fingerprintsPrologue = this.generateNoisePrologue(peerConnection, remoteCerthash.name, ma)
|
|
169
|
+
|
|
170
|
+
// Since we use the default crypto interface and do not use a static key or early data,
|
|
171
|
+
// we pass in undefined for these parameters.
|
|
172
|
+
const noiseInit = { staticNoiseKey: undefined, extensions: undefined, crypto: undefined, prologueBytes: fingerprintsPrologue }
|
|
173
|
+
const noise = Noise(noiseInit)()
|
|
174
|
+
const wrappedChannel = new WebRTCStream({ channel: handshakeDataChannel, stat: { direction: 'outbound', timeline: { open: 1 } } })
|
|
175
|
+
const wrappedDuplex = {
|
|
176
|
+
...wrappedChannel,
|
|
177
|
+
source: {
|
|
178
|
+
[Symbol.asyncIterator]: async function * () {
|
|
179
|
+
for await (const list of wrappedChannel.source) {
|
|
180
|
+
yield list.subarray()
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Creating the connection before completion of the noise
|
|
187
|
+
// handshake ensures that the stream opening callback is set up
|
|
188
|
+
const maConn = new WebRTCMultiaddrConnection({
|
|
189
|
+
peerConnection,
|
|
190
|
+
remoteAddr: ma,
|
|
191
|
+
timeline: {
|
|
192
|
+
open: (new Date()).getTime()
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
const muxerFactory = new DataChannelMuxerFactory(peerConnection)
|
|
197
|
+
|
|
198
|
+
// For outbound connections, the remote is expected to start the noise handshake.
|
|
199
|
+
// Therefore, we need to secure an inbound noise connection from the remote.
|
|
200
|
+
await noise.secureInbound(myPeerId, wrappedDuplex, theirPeerId)
|
|
201
|
+
|
|
202
|
+
return await options.upgrader.upgradeOutbound(maConn, { skipProtection: true, skipEncryption: true, muxerFactory })
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Generate a noise prologue from the peer connection's certificate.
|
|
207
|
+
* noise prologue = bytes('libp2p-webrtc-noise:') + noise-responder fingerprint + noise-initiator fingerprint
|
|
208
|
+
*/
|
|
209
|
+
private generateNoisePrologue (pc: RTCPeerConnection, hashName: multihashes.HashName, ma: Multiaddr): Uint8Array {
|
|
210
|
+
if (pc.getConfiguration().certificates?.length === 0) {
|
|
211
|
+
throw invalidArgument('no local certificate')
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const localCert = pc.getConfiguration().certificates?.at(0)
|
|
215
|
+
|
|
216
|
+
if (localCert === undefined || localCert.getFingerprints().length === 0) {
|
|
217
|
+
throw invalidArgument('no fingerprint on local certificate')
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const localFingerprint = localCert.getFingerprints()[0]
|
|
221
|
+
|
|
222
|
+
if (localFingerprint.value === undefined) {
|
|
223
|
+
throw invalidArgument('no fingerprint on local certificate')
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const localFpString = localFingerprint.value.replace(/:/g, '')
|
|
227
|
+
const localFpArray = uint8arrayFromString(localFpString, 'hex')
|
|
228
|
+
const local = multihashes.encode(localFpArray, multihashes.names[hashName])
|
|
229
|
+
const remote: Uint8Array = sdp.mbdecoder.decode(sdp.certhash(ma))
|
|
230
|
+
const prefix = uint8arrayFromString('libp2p-webrtc-noise:')
|
|
231
|
+
|
|
232
|
+
return concat([prefix, local, remote])
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Determine if a given multiaddr contains a WebRTC Code (280),
|
|
238
|
+
* a Certhash Code (466) and a PeerId
|
|
239
|
+
*/
|
|
240
|
+
function validMa (ma: Multiaddr): boolean {
|
|
241
|
+
const codes = ma.protoCodes()
|
|
242
|
+
return codes.includes(WEBRTC_CODE) && codes.includes(CERTHASH_CODE) && ma.getPeerId() != null
|
|
243
|
+
}
|