30
30
*/
31
31
32
32
import { noise } from '@chainsafe/libp2p-noise'
33
- import { type Transport , transportSymbol , type CreateListenerOptions , type DialOptions , type Listener , type ComponentLogger , type Logger , type Connection , type MultiaddrConnection , type Stream , type CounterGroup , type Metrics , type PeerId , type StreamMuxerFactory , type StreamMuxerInit , type StreamMuxer } from '@libp2p/interface'
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 { webtransportBiDiStreamToStream } from './stream.js'
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
- maxInboundStreams : init . maxInboundStreams ?? 1000
87
+ ...init ,
88
+ maxInboundStreams : init . maxInboundStreams ?? 1000 ,
89
+ certificates : init . certificates ?? [ ]
73
90
}
74
91
75
92
if ( components . metrics != null ) {
@@ -87,24 +104,26 @@ class WebTransportTransport implements Transport {
87
104
readonly [ transportSymbol ] = true
88
105
89
106
async dial ( ma : Multiaddr , options : DialOptions ) : Promise < Connection > {
90
- options ?. signal ?. throwIfAborted ( )
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 Error ( 'Need a local peerid' )
114
+ throw new CodeError ( 'Need a local peerid' , 'ERR_INVALID_PARAMETERS ')
96
115
}
97
116
98
117
options = options ?? { }
99
118
100
119
const { url, certhashes, remotePeer } = parseMultiaddr ( ma )
101
120
102
121
if ( remotePeer == null ) {
103
- throw new Error ( 'Need a target peerid' )
122
+ throw new CodeError ( 'Need a target peerid' , 'ERR_INVALID_PARAMETERS ')
104
123
}
105
124
106
125
if ( certhashes . length === 0 ) {
107
- throw new Error ( 'Expected multiaddr to contain certhashes' )
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
- if ( ! await this . authenticateWebTransport ( wt , localPeer , remotePeer , certhashes ) ) {
179
- throw new Error ( 'Failed to authenticate webtransport' )
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 ( 'Closing webtransport' )
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
- authenticated = true
203
-
204
- return await options . upgrader . upgradeOutbound ( maConn , { skipEncryption : true , muxerFactory : this . webtransportMuxer ( wt ) , skipProtection : true } )
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 : InstanceType < typeof WebTransport > , localPeer : PeerId , remotePeer : PeerId , certhashes : Array < MultihashDigest < number > > ) : Promise < boolean > {
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 function ( source : Source < Uint8Array | Uint8ArrayList > ) {
272
+ sink : async ( source : Source < Uint8Array | Uint8ArrayList > ) => {
245
273
for await ( const chunk of source ) {
246
- if ( chunk instanceof Uint8Array ) {
247
- await writer . write ( chunk )
248
- } else {
249
- await writer . write ( chunk . subarray ( ) )
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,105 +303,12 @@ class WebTransportTransport implements Transport {
273
303
return true
274
304
}
275
305
276
- webtransportMuxer ( wt : WebTransport ) : StreamMuxerFactory {
277
- let streamIDCounter = 0
278
- const config = this . config
279
- const self = this
280
- return {
281
- protocol : 'webtransport' ,
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
- }
371
- }
372
-
373
306
createListener ( options : CreateListenerOptions ) : Listener {
374
- throw new Error ( 'Webtransport servers are not supported in Node or the browser' )
307
+ return createListener ( this . components , {
308
+ ...options ,
309
+ certificates : this . config . certificates ,
310
+ maxInboundStreams : this . config . maxInboundStreams
311
+ } )
375
312
}
376
313
377
314
/**
0 commit comments