@libp2p/webtransport 4.0.28 → 4.0.29
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/dist/index.min.js +3 -3
- package/dist/src/index.d.ts +12 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +55 -94
- package/dist/src/index.js.map +1 -1
- package/dist/src/listener.browser.d.ts +3 -0
- package/dist/src/listener.browser.d.ts.map +1 -0
- package/dist/src/listener.browser.js +4 -0
- package/dist/src/listener.browser.js.map +1 -0
- package/dist/src/listener.d.ts +15 -0
- package/dist/src/listener.d.ts.map +1 -0
- package/dist/src/listener.js +4 -0
- package/dist/src/listener.js.map +1 -0
- package/dist/src/muxer.d.ts +7 -0
- package/dist/src/muxer.d.ts.map +1 -0
- package/dist/src/muxer.js +70 -0
- package/dist/src/muxer.js.map +1 -0
- package/dist/src/stream.d.ts.map +1 -1
- package/dist/src/stream.js +79 -151
- package/dist/src/stream.js.map +1 -1
- package/dist/src/utils/generate-certificates.browser.d.ts +2 -0
- package/dist/src/utils/generate-certificates.browser.d.ts.map +1 -0
- package/dist/src/utils/generate-certificates.browser.js +4 -0
- package/dist/src/utils/generate-certificates.browser.js.map +1 -0
- package/dist/src/utils/generate-certificates.d.ts +8 -0
- package/dist/src/utils/generate-certificates.d.ts.map +1 -0
- package/dist/src/utils/generate-certificates.js +4 -0
- package/dist/src/utils/generate-certificates.js.map +1 -0
- package/dist/src/webtransport.browser.d.ts +2 -0
- package/dist/src/webtransport.browser.d.ts.map +1 -0
- package/dist/src/webtransport.browser.js +2 -0
- package/dist/src/webtransport.browser.js.map +1 -0
- package/dist/src/webtransport.d.ts +9 -0
- package/dist/src/webtransport.d.ts.map +1 -0
- package/dist/src/webtransport.js +15 -0
- package/dist/src/webtransport.js.map +1 -0
- package/dist/typedoc-urls.json +2 -0
- package/package.json +18 -9
- package/src/index.ts +74 -122
- package/src/listener.browser.ts +5 -0
- package/src/listener.ts +19 -0
- package/src/muxer.ts +95 -0
- package/src/stream.ts +90 -164
- package/src/utils/generate-certificates.browser.ts +3 -0
- package/src/utils/generate-certificates.ts +11 -0
- package/src/webtransport.browser.ts +1 -0
- package/src/webtransport.ts +17 -0
package/src/index.ts
CHANGED
|
@@ -30,23 +30,38 @@
|
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
32
|
import { noise } from '@chainsafe/libp2p-noise'
|
|
33
|
-
import {
|
|
34
|
-
import { type Multiaddr, type AbortOptions } from '@multiformats/multiaddr'
|
|
33
|
+
import { AbortError, CodeError, transportSymbol } from '@libp2p/interface'
|
|
35
34
|
import { WebTransport as WebTransportMatcher } from '@multiformats/multiaddr-matcher'
|
|
36
|
-
import {
|
|
35
|
+
import { raceSignal } from 'race-signal'
|
|
36
|
+
import createListener from './listener.js'
|
|
37
|
+
import { webtransportMuxer } from './muxer.js'
|
|
37
38
|
import { inertDuplex } from './utils/inert-duplex.js'
|
|
38
39
|
import { isSubset } from './utils/is-subset.js'
|
|
39
40
|
import { parseMultiaddr } from './utils/parse-multiaddr.js'
|
|
41
|
+
import WebTransport from './webtransport.js'
|
|
42
|
+
import type { Transport, CreateListenerOptions, DialOptions, Listener, ComponentLogger, Logger, Connection, MultiaddrConnection, CounterGroup, Metrics, PeerId } from '@libp2p/interface'
|
|
43
|
+
import type { Multiaddr } from '@multiformats/multiaddr'
|
|
40
44
|
import type { Source } from 'it-stream-types'
|
|
41
45
|
import type { MultihashDigest } from 'multiformats/hashes/interface'
|
|
42
46
|
import type { Uint8ArrayList } from 'uint8arraylist'
|
|
43
47
|
|
|
48
|
+
/**
|
|
49
|
+
* PEM format server certificate and private key
|
|
50
|
+
*/
|
|
51
|
+
export interface WebTransportCertificate {
|
|
52
|
+
privateKey: string
|
|
53
|
+
pem: string
|
|
54
|
+
hash: MultihashDigest<number>
|
|
55
|
+
secret: string
|
|
56
|
+
}
|
|
57
|
+
|
|
44
58
|
interface WebTransportSessionCleanup {
|
|
45
59
|
(metric: string): void
|
|
46
60
|
}
|
|
47
61
|
|
|
48
62
|
export interface WebTransportInit {
|
|
49
63
|
maxInboundStreams?: number
|
|
64
|
+
certificates?: WebTransportCertificate[]
|
|
50
65
|
}
|
|
51
66
|
|
|
52
67
|
export interface WebTransportComponents {
|
|
@@ -69,7 +84,9 @@ class WebTransportTransport implements Transport {
|
|
|
69
84
|
this.log = components.logger.forComponent('libp2p:webtransport')
|
|
70
85
|
this.components = components
|
|
71
86
|
this.config = {
|
|
72
|
-
|
|
87
|
+
...init,
|
|
88
|
+
maxInboundStreams: init.maxInboundStreams ?? 1000,
|
|
89
|
+
certificates: init.certificates ?? []
|
|
73
90
|
}
|
|
74
91
|
|
|
75
92
|
if (components.metrics != null) {
|
|
@@ -87,12 +104,14 @@ class WebTransportTransport implements Transport {
|
|
|
87
104
|
readonly [transportSymbol] = true
|
|
88
105
|
|
|
89
106
|
async dial (ma: Multiaddr, options: DialOptions): Promise<Connection> {
|
|
90
|
-
options?.signal?.
|
|
107
|
+
if (options?.signal?.aborted === true) {
|
|
108
|
+
throw new AbortError()
|
|
109
|
+
}
|
|
91
110
|
|
|
92
111
|
this.log('dialing %s', ma)
|
|
93
112
|
const localPeer = this.components.peerId
|
|
94
113
|
if (localPeer === undefined) {
|
|
95
|
-
throw new
|
|
114
|
+
throw new CodeError('Need a local peerid', 'ERR_INVALID_PARAMETERS')
|
|
96
115
|
}
|
|
97
116
|
|
|
98
117
|
options = options ?? {}
|
|
@@ -100,11 +119,11 @@ class WebTransportTransport implements Transport {
|
|
|
100
119
|
const { url, certhashes, remotePeer } = parseMultiaddr(ma)
|
|
101
120
|
|
|
102
121
|
if (remotePeer == null) {
|
|
103
|
-
throw new
|
|
122
|
+
throw new CodeError('Need a target peerid', 'ERR_INVALID_PARAMETERS')
|
|
104
123
|
}
|
|
105
124
|
|
|
106
125
|
if (certhashes.length === 0) {
|
|
107
|
-
throw new
|
|
126
|
+
throw new CodeError('Expected multiaddr to contain certhashes', 'ERR_INVALID_PARAMETERS')
|
|
108
127
|
}
|
|
109
128
|
|
|
110
129
|
let abortListener: (() => void) | undefined
|
|
@@ -159,10 +178,12 @@ class WebTransportTransport implements Transport {
|
|
|
159
178
|
once: true
|
|
160
179
|
})
|
|
161
180
|
|
|
181
|
+
this.log('wait for session to be ready')
|
|
162
182
|
await Promise.race([
|
|
163
183
|
wt.closed,
|
|
164
184
|
wt.ready
|
|
165
185
|
])
|
|
186
|
+
this.log('session became ready')
|
|
166
187
|
|
|
167
188
|
ready = true
|
|
168
189
|
this.metrics?.dialerEvents.increment({ ready: true })
|
|
@@ -175,15 +196,17 @@ class WebTransportTransport implements Transport {
|
|
|
175
196
|
cleanUpWTSession('remote_close')
|
|
176
197
|
})
|
|
177
198
|
|
|
178
|
-
|
|
179
|
-
|
|
199
|
+
authenticated = await raceSignal(this.authenticateWebTransport(wt, localPeer, remotePeer, certhashes), options.signal)
|
|
200
|
+
|
|
201
|
+
if (!authenticated) {
|
|
202
|
+
throw new CodeError('Failed to authenticate webtransport', 'ERR_AUTHENTICATION_FAILED')
|
|
180
203
|
}
|
|
181
204
|
|
|
182
205
|
this.metrics?.dialerEvents.increment({ open: true })
|
|
183
206
|
|
|
184
207
|
maConn = {
|
|
185
208
|
close: async () => {
|
|
186
|
-
this.log('
|
|
209
|
+
this.log('closing webtransport')
|
|
187
210
|
cleanUpWTSession('close')
|
|
188
211
|
},
|
|
189
212
|
abort: (err: Error) => {
|
|
@@ -199,9 +222,11 @@ class WebTransportTransport implements Transport {
|
|
|
199
222
|
...inertDuplex()
|
|
200
223
|
}
|
|
201
224
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
225
|
+
return await options.upgrader.upgradeOutbound(maConn, {
|
|
226
|
+
skipEncryption: true,
|
|
227
|
+
muxerFactory: webtransportMuxer(wt, wt.incomingBidirectionalStreams.getReader(), this.components.logger, this.config),
|
|
228
|
+
skipProtection: true
|
|
229
|
+
})
|
|
205
230
|
} catch (err: any) {
|
|
206
231
|
this.log.error('caught wt session err', err)
|
|
207
232
|
|
|
@@ -221,11 +246,14 @@ class WebTransportTransport implements Transport {
|
|
|
221
246
|
}
|
|
222
247
|
}
|
|
223
248
|
|
|
224
|
-
async authenticateWebTransport (wt:
|
|
249
|
+
async authenticateWebTransport (wt: WebTransport, localPeer: PeerId, remotePeer: PeerId, certhashes: Array<MultihashDigest<number>>, signal?: AbortSignal): Promise<boolean> {
|
|
250
|
+
if (signal?.aborted === true) {
|
|
251
|
+
throw new AbortError()
|
|
252
|
+
}
|
|
253
|
+
|
|
225
254
|
const stream = await wt.createBidirectionalStream()
|
|
226
255
|
const writer = stream.writable.getWriter()
|
|
227
256
|
const reader = stream.readable.getReader()
|
|
228
|
-
await writer.ready
|
|
229
257
|
|
|
230
258
|
const duplex = {
|
|
231
259
|
source: (async function * () {
|
|
@@ -241,13 +269,15 @@ class WebTransportTransport implements Transport {
|
|
|
241
269
|
}
|
|
242
270
|
}
|
|
243
271
|
})(),
|
|
244
|
-
sink: async
|
|
272
|
+
sink: async (source: Source<Uint8Array | Uint8ArrayList>) => {
|
|
245
273
|
for await (const chunk of source) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
274
|
+
await raceSignal(writer.ready, signal)
|
|
275
|
+
|
|
276
|
+
const buf = chunk instanceof Uint8Array ? chunk : chunk.subarray()
|
|
277
|
+
|
|
278
|
+
writer.write(buf).catch(err => {
|
|
279
|
+
this.log.error('could not write chunk during authentication of WebTransport stream', err)
|
|
280
|
+
})
|
|
251
281
|
}
|
|
252
282
|
}
|
|
253
283
|
}
|
|
@@ -273,112 +303,34 @@ class WebTransportTransport implements Transport {
|
|
|
273
303
|
return true
|
|
274
304
|
}
|
|
275
305
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
createStreamMuxer: (init?: StreamMuxerInit): StreamMuxer => {
|
|
283
|
-
// !TODO handle abort signal when WebTransport supports this.
|
|
284
|
-
|
|
285
|
-
if (typeof init === 'function') {
|
|
286
|
-
// The api docs say that init may be a function
|
|
287
|
-
init = { onIncomingStream: init }
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
const activeStreams: Stream[] = [];
|
|
291
|
-
|
|
292
|
-
(async function () {
|
|
293
|
-
//! TODO unclear how to add backpressure here?
|
|
294
|
-
|
|
295
|
-
const reader = wt.incomingBidirectionalStreams.getReader()
|
|
296
|
-
while (true) {
|
|
297
|
-
const { done, value: wtStream } = await reader.read()
|
|
298
|
-
|
|
299
|
-
if (done) {
|
|
300
|
-
break
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
if (activeStreams.length >= config.maxInboundStreams) {
|
|
304
|
-
// We've reached our limit, close this stream.
|
|
305
|
-
wtStream.writable.close().catch((err: Error) => {
|
|
306
|
-
self.log.error(`Failed to close inbound stream that crossed our maxInboundStream limit: ${err.message}`)
|
|
307
|
-
})
|
|
308
|
-
wtStream.readable.cancel().catch((err: Error) => {
|
|
309
|
-
self.log.error(`Failed to close inbound stream that crossed our maxInboundStream limit: ${err.message}`)
|
|
310
|
-
})
|
|
311
|
-
} else {
|
|
312
|
-
const stream = await webtransportBiDiStreamToStream(
|
|
313
|
-
wtStream,
|
|
314
|
-
String(streamIDCounter++),
|
|
315
|
-
'inbound',
|
|
316
|
-
activeStreams,
|
|
317
|
-
init?.onStreamEnd,
|
|
318
|
-
self.components.logger
|
|
319
|
-
)
|
|
320
|
-
activeStreams.push(stream)
|
|
321
|
-
init?.onIncomingStream?.(stream)
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
})().catch(() => {
|
|
325
|
-
this.log.error('WebTransport failed to receive incoming stream')
|
|
326
|
-
})
|
|
327
|
-
|
|
328
|
-
const muxer: StreamMuxer = {
|
|
329
|
-
protocol: 'webtransport',
|
|
330
|
-
streams: activeStreams,
|
|
331
|
-
newStream: async (name?: string): Promise<Stream> => {
|
|
332
|
-
const wtStream = await wt.createBidirectionalStream()
|
|
333
|
-
|
|
334
|
-
const stream = await webtransportBiDiStreamToStream(
|
|
335
|
-
wtStream,
|
|
336
|
-
String(streamIDCounter++),
|
|
337
|
-
init?.direction ?? 'outbound',
|
|
338
|
-
activeStreams,
|
|
339
|
-
init?.onStreamEnd,
|
|
340
|
-
self.components.logger
|
|
341
|
-
)
|
|
342
|
-
activeStreams.push(stream)
|
|
343
|
-
|
|
344
|
-
return stream
|
|
345
|
-
},
|
|
346
|
-
|
|
347
|
-
/**
|
|
348
|
-
* Close or abort all tracked streams and stop the muxer
|
|
349
|
-
*/
|
|
350
|
-
close: async (options?: AbortOptions) => {
|
|
351
|
-
this.log('Closing webtransport muxer')
|
|
352
|
-
|
|
353
|
-
await Promise.all(
|
|
354
|
-
activeStreams.map(async s => s.close(options))
|
|
355
|
-
)
|
|
356
|
-
},
|
|
357
|
-
abort: (err: Error) => {
|
|
358
|
-
this.log('Aborting webtransport muxer with err:', err)
|
|
359
|
-
|
|
360
|
-
for (const stream of activeStreams) {
|
|
361
|
-
stream.abort(err)
|
|
362
|
-
}
|
|
363
|
-
},
|
|
364
|
-
// This stream muxer is webtransport native. Therefore it doesn't plug in with any other duplex.
|
|
365
|
-
...inertDuplex()
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
return muxer
|
|
369
|
-
}
|
|
370
|
-
}
|
|
306
|
+
createListener (options: CreateListenerOptions): Listener {
|
|
307
|
+
return createListener(this.components, {
|
|
308
|
+
...options,
|
|
309
|
+
certificates: this.config.certificates,
|
|
310
|
+
maxInboundStreams: this.config.maxInboundStreams
|
|
311
|
+
})
|
|
371
312
|
}
|
|
372
313
|
|
|
373
|
-
|
|
374
|
-
|
|
314
|
+
/**
|
|
315
|
+
* Filter check for all Multiaddrs that this transport can listen on
|
|
316
|
+
*/
|
|
317
|
+
listenFilter (): Multiaddr[] {
|
|
318
|
+
return []
|
|
375
319
|
}
|
|
376
320
|
|
|
377
321
|
/**
|
|
378
|
-
*
|
|
322
|
+
* Filter check for all Multiaddrs that this transport can dial
|
|
379
323
|
*/
|
|
380
|
-
|
|
381
|
-
return multiaddrs.filter(
|
|
324
|
+
dialFilter (multiaddrs: Multiaddr[]): Multiaddr[] {
|
|
325
|
+
return multiaddrs.filter(ma => {
|
|
326
|
+
if (!WebTransportMatcher.exactMatch(ma)) {
|
|
327
|
+
return false
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const { url, certhashes, remotePeer } = parseMultiaddr(ma)
|
|
331
|
+
|
|
332
|
+
return url != null && remotePeer != null && certhashes.length > 0
|
|
333
|
+
})
|
|
382
334
|
}
|
|
383
335
|
}
|
|
384
336
|
|
package/src/listener.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { WebTransportCertificate } from './index.js'
|
|
2
|
+
import type { Connection, Upgrader, Listener, CreateListenerOptions, PeerId, ComponentLogger, Metrics } from '@libp2p/interface'
|
|
3
|
+
|
|
4
|
+
export interface WebTransportListenerComponents {
|
|
5
|
+
peerId: PeerId
|
|
6
|
+
logger: ComponentLogger
|
|
7
|
+
metrics?: Metrics
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface WebTransportListenerInit extends CreateListenerOptions {
|
|
11
|
+
handler?(conn: Connection): void
|
|
12
|
+
upgrader: Upgrader
|
|
13
|
+
certificates?: WebTransportCertificate[]
|
|
14
|
+
maxInboundStreams?: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function createListener (components: WebTransportListenerComponents, options: WebTransportListenerInit): Listener {
|
|
18
|
+
throw new Error('Only supported in browsers')
|
|
19
|
+
}
|
package/src/muxer.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { webtransportBiDiStreamToStream } from './stream.js'
|
|
2
|
+
import { inertDuplex } from './utils/inert-duplex.js'
|
|
3
|
+
import type WebTransport from './webtransport.js'
|
|
4
|
+
import type { ComponentLogger, Stream, StreamMuxer, StreamMuxerFactory, StreamMuxerInit } from '@libp2p/interface'
|
|
5
|
+
|
|
6
|
+
export interface WebTransportMuxerInit {
|
|
7
|
+
maxInboundStreams: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function webtransportMuxer (wt: Pick<WebTransport, 'close' | 'createBidirectionalStream'>, reader: ReadableStreamDefaultReader<WebTransportBidirectionalStream>, logger: ComponentLogger, config: WebTransportMuxerInit): StreamMuxerFactory {
|
|
11
|
+
let streamIDCounter = 0
|
|
12
|
+
const log = logger.forComponent('libp2p:webtransport:muxer')
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
protocol: 'webtransport',
|
|
16
|
+
createStreamMuxer: (init?: StreamMuxerInit): StreamMuxer => {
|
|
17
|
+
// !TODO handle abort signal when WebTransport supports this.
|
|
18
|
+
|
|
19
|
+
if (typeof init === 'function') {
|
|
20
|
+
// The api docs say that init may be a function
|
|
21
|
+
init = { onIncomingStream: init }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const activeStreams: Stream[] = []
|
|
25
|
+
|
|
26
|
+
void Promise.resolve().then(async () => {
|
|
27
|
+
//! TODO unclear how to add backpressure here?
|
|
28
|
+
while (true) {
|
|
29
|
+
const { done, value: wtStream } = await reader.read()
|
|
30
|
+
|
|
31
|
+
if (done) {
|
|
32
|
+
break
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (activeStreams.length >= config.maxInboundStreams) {
|
|
36
|
+
log(`too many inbound streams open - ${activeStreams.length}/${config.maxInboundStreams}, closing new incoming stream`)
|
|
37
|
+
// We've reached our limit, close this stream.
|
|
38
|
+
wtStream.writable.close().catch((err: Error) => {
|
|
39
|
+
log.error(`failed to close inbound stream that crossed our maxInboundStream limit: ${err.message}`)
|
|
40
|
+
})
|
|
41
|
+
wtStream.readable.cancel().catch((err: Error) => {
|
|
42
|
+
log.error(`failed to close inbound stream that crossed our maxInboundStream limit: ${err.message}`)
|
|
43
|
+
})
|
|
44
|
+
} else {
|
|
45
|
+
const stream = await webtransportBiDiStreamToStream(
|
|
46
|
+
wtStream,
|
|
47
|
+
String(streamIDCounter++),
|
|
48
|
+
'inbound',
|
|
49
|
+
activeStreams,
|
|
50
|
+
init?.onStreamEnd,
|
|
51
|
+
logger
|
|
52
|
+
)
|
|
53
|
+
activeStreams.push(stream)
|
|
54
|
+
init?.onIncomingStream?.(stream)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const muxer: StreamMuxer = {
|
|
60
|
+
protocol: 'webtransport',
|
|
61
|
+
streams: activeStreams,
|
|
62
|
+
newStream: async (name?: string): Promise<Stream> => {
|
|
63
|
+
log('new outgoing stream', name)
|
|
64
|
+
|
|
65
|
+
const wtStream = await wt.createBidirectionalStream()
|
|
66
|
+
const stream = await webtransportBiDiStreamToStream(wtStream, String(streamIDCounter++), init?.direction ?? 'outbound', activeStreams, init?.onStreamEnd, logger)
|
|
67
|
+
activeStreams.push(stream)
|
|
68
|
+
|
|
69
|
+
return stream
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Close all tracked streams and stop the muxer
|
|
74
|
+
*/
|
|
75
|
+
close: async () => {
|
|
76
|
+
log('closing webtransport muxer gracefully')
|
|
77
|
+
wt.close()
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Abort all tracked streams and stop the muxer
|
|
82
|
+
*/
|
|
83
|
+
abort: (err: Error) => {
|
|
84
|
+
log('closing webtransport muxer with err:', err)
|
|
85
|
+
wt.close()
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
// This stream muxer is webtransport native. Therefore it doesn't plug in with any other duplex.
|
|
89
|
+
...inertDuplex()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return muxer
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|