@libp2p/webtransport 3.0.3 → 3.0.4-4ef9c79c
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 +2 -0
- package/dist/index.min.js +17 -14
- package/dist/src/index.d.ts +0 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +106 -311
- package/dist/src/index.js.map +1 -1
- package/dist/src/stream.d.ts +3 -0
- package/dist/src/stream.d.ts.map +1 -0
- package/dist/src/stream.js +158 -0
- package/dist/src/stream.js.map +1 -0
- package/dist/src/utils/inert-duplex.d.ts +3 -0
- package/dist/src/utils/inert-duplex.d.ts.map +1 -0
- package/dist/src/utils/inert-duplex.js +20 -0
- package/dist/src/utils/inert-duplex.js.map +1 -0
- package/dist/src/utils/is-subset.d.ts +6 -0
- package/dist/src/utils/is-subset.d.ts.map +1 -0
- package/dist/src/utils/is-subset.js +12 -0
- package/dist/src/utils/is-subset.js.map +1 -0
- package/dist/src/utils/parse-multiaddr.d.ts +9 -0
- package/dist/src/utils/parse-multiaddr.d.ts.map +1 -0
- package/dist/src/utils/parse-multiaddr.js +77 -0
- package/dist/src/utils/parse-multiaddr.js.map +1 -0
- package/package.json +9 -8
- package/src/index.ts +127 -343
- package/src/stream.ts +183 -0
- package/src/utils/inert-duplex.ts +21 -0
- package/src/utils/is-subset.ts +12 -0
- package/src/utils/parse-multiaddr.ts +83 -0
- package/dist/typedoc-urls.json +0 -6
package/src/index.ts
CHANGED
|
@@ -1,315 +1,32 @@
|
|
|
1
1
|
import { noise } from '@chainsafe/libp2p-noise'
|
|
2
2
|
import { type Transport, symbol, type CreateListenerOptions, type DialOptions, type Listener } from '@libp2p/interface/transport'
|
|
3
3
|
import { logger } from '@libp2p/logger'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
4
|
+
import { type Multiaddr, type AbortOptions } from '@multiformats/multiaddr'
|
|
5
|
+
import { webtransportBiDiStreamToStream } from './stream.js'
|
|
6
|
+
import { inertDuplex } from './utils/inert-duplex.js'
|
|
7
|
+
import { isSubset } from './utils/is-subset.js'
|
|
8
|
+
import { parseMultiaddr } from './utils/parse-multiaddr.js'
|
|
9
|
+
import type { Connection, MultiaddrConnection, Stream } from '@libp2p/interface/connection'
|
|
9
10
|
import type { PeerId } from '@libp2p/interface/peer-id'
|
|
10
11
|
import type { StreamMuxerFactory, StreamMuxerInit, StreamMuxer } from '@libp2p/interface/stream-muxer'
|
|
11
|
-
import type {
|
|
12
|
+
import type { Source } from 'it-stream-types'
|
|
12
13
|
import type { MultihashDigest } from 'multiformats/hashes/interface'
|
|
13
14
|
|
|
14
15
|
declare global {
|
|
15
16
|
var WebTransport: any
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
function decodeCerthashStr (s: string): MultihashDigest {
|
|
24
|
-
return digest.decode(multibaseDecoder.decode(s))
|
|
19
|
+
// https://www.w3.org/TR/webtransport/#web-transport-close-info
|
|
20
|
+
interface WebTransportCloseInfo {
|
|
21
|
+
closeCode: number
|
|
22
|
+
reason: string
|
|
25
23
|
}
|
|
26
24
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return {
|
|
30
|
-
source: {
|
|
31
|
-
[Symbol.asyncIterator] () {
|
|
32
|
-
return {
|
|
33
|
-
async next () {
|
|
34
|
-
// This will never resolve
|
|
35
|
-
return new Promise(() => { })
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
|
-
sink: async (source: Source<any>) => {
|
|
41
|
-
// This will never resolve
|
|
42
|
-
return new Promise(() => { })
|
|
43
|
-
}
|
|
44
|
-
}
|
|
25
|
+
interface WebTransportSessionCleanup {
|
|
26
|
+
(closeInfo?: WebTransportCloseInfo): void
|
|
45
27
|
}
|
|
46
28
|
|
|
47
|
-
|
|
48
|
-
const writer = bidiStream.writable.getWriter()
|
|
49
|
-
const reader = bidiStream.readable.getReader()
|
|
50
|
-
await writer.ready
|
|
51
|
-
|
|
52
|
-
function cleanupStreamFromActiveStreams (): void {
|
|
53
|
-
const index = activeStreams.findIndex(s => s === stream)
|
|
54
|
-
if (index !== -1) {
|
|
55
|
-
activeStreams.splice(index, 1)
|
|
56
|
-
stream.timeline.close = Date.now()
|
|
57
|
-
onStreamEnd?.(stream)
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
let writerClosed = false
|
|
62
|
-
let readerClosed = false;
|
|
63
|
-
(async function () {
|
|
64
|
-
const err: Error | undefined = await writer.closed.catch((err: Error) => err)
|
|
65
|
-
if (err != null) {
|
|
66
|
-
const msg = err.message
|
|
67
|
-
if (!(msg.includes('aborted by the remote server') || msg.includes('STOP_SENDING'))) {
|
|
68
|
-
log.error(`WebTransport writer closed unexpectedly: streamId=${streamId} err=${err.message}`)
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
writerClosed = true
|
|
72
|
-
if (writerClosed && readerClosed) {
|
|
73
|
-
cleanupStreamFromActiveStreams()
|
|
74
|
-
}
|
|
75
|
-
})().catch(() => {
|
|
76
|
-
log.error('WebTransport failed to cleanup closed stream')
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
(async function () {
|
|
80
|
-
const err: Error | undefined = await reader.closed.catch((err: Error) => err)
|
|
81
|
-
if (err != null) {
|
|
82
|
-
log.error(`WebTransport reader closed unexpectedly: streamId=${streamId} err=${err.message}`)
|
|
83
|
-
}
|
|
84
|
-
readerClosed = true
|
|
85
|
-
if (writerClosed && readerClosed) {
|
|
86
|
-
cleanupStreamFromActiveStreams()
|
|
87
|
-
}
|
|
88
|
-
})().catch(() => {
|
|
89
|
-
log.error('WebTransport failed to cleanup closed stream')
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
let sinkSunk = false
|
|
93
|
-
const stream: Stream = {
|
|
94
|
-
id: streamId,
|
|
95
|
-
status: 'open',
|
|
96
|
-
writeStatus: 'ready',
|
|
97
|
-
readStatus: 'ready',
|
|
98
|
-
abort (err: Error) {
|
|
99
|
-
if (!writerClosed) {
|
|
100
|
-
writer.abort()
|
|
101
|
-
writerClosed = true
|
|
102
|
-
}
|
|
103
|
-
stream.abort(err)
|
|
104
|
-
readerClosed = true
|
|
105
|
-
|
|
106
|
-
this.status = 'aborted'
|
|
107
|
-
this.writeStatus = 'closed'
|
|
108
|
-
this.readStatus = 'closed'
|
|
109
|
-
|
|
110
|
-
this.timeline.reset =
|
|
111
|
-
this.timeline.close =
|
|
112
|
-
this.timeline.closeRead =
|
|
113
|
-
this.timeline.closeWrite = Date.now()
|
|
114
|
-
|
|
115
|
-
cleanupStreamFromActiveStreams()
|
|
116
|
-
},
|
|
117
|
-
async close (options?: AbortOptions) {
|
|
118
|
-
this.status = 'closing'
|
|
119
|
-
|
|
120
|
-
await Promise.all([
|
|
121
|
-
stream.closeRead(options),
|
|
122
|
-
stream.closeWrite(options)
|
|
123
|
-
])
|
|
124
|
-
|
|
125
|
-
cleanupStreamFromActiveStreams()
|
|
126
|
-
|
|
127
|
-
this.status = 'closed'
|
|
128
|
-
this.timeline.close = Date.now()
|
|
129
|
-
},
|
|
130
|
-
|
|
131
|
-
async closeRead (options?: AbortOptions) {
|
|
132
|
-
if (!readerClosed) {
|
|
133
|
-
this.readStatus = 'closing'
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
await reader.cancel()
|
|
137
|
-
} catch (err: any) {
|
|
138
|
-
if (err.toString().includes('RESET_STREAM') === true) {
|
|
139
|
-
writerClosed = true
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
this.timeline.closeRead = Date.now()
|
|
144
|
-
this.readStatus = 'closed'
|
|
145
|
-
|
|
146
|
-
readerClosed = true
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (writerClosed) {
|
|
150
|
-
cleanupStreamFromActiveStreams()
|
|
151
|
-
}
|
|
152
|
-
},
|
|
153
|
-
|
|
154
|
-
async closeWrite (options?: AbortOptions) {
|
|
155
|
-
if (!writerClosed) {
|
|
156
|
-
writerClosed = true
|
|
157
|
-
|
|
158
|
-
this.writeStatus = 'closing'
|
|
159
|
-
|
|
160
|
-
try {
|
|
161
|
-
await writer.close()
|
|
162
|
-
} catch (err: any) {
|
|
163
|
-
if (err.toString().includes('RESET_STREAM') === true) {
|
|
164
|
-
readerClosed = true
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
this.timeline.closeWrite = Date.now()
|
|
169
|
-
this.writeStatus = 'closed'
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (readerClosed) {
|
|
173
|
-
cleanupStreamFromActiveStreams()
|
|
174
|
-
}
|
|
175
|
-
},
|
|
176
|
-
direction,
|
|
177
|
-
timeline: { open: Date.now() },
|
|
178
|
-
metadata: {},
|
|
179
|
-
source: (async function * () {
|
|
180
|
-
while (true) {
|
|
181
|
-
const val = await reader.read()
|
|
182
|
-
if (val.done === true) {
|
|
183
|
-
readerClosed = true
|
|
184
|
-
if (writerClosed) {
|
|
185
|
-
cleanupStreamFromActiveStreams()
|
|
186
|
-
}
|
|
187
|
-
return
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
yield new Uint8ArrayList(val.value)
|
|
191
|
-
}
|
|
192
|
-
})(),
|
|
193
|
-
sink: async function (source: Source<Uint8Array | Uint8ArrayList>) {
|
|
194
|
-
if (sinkSunk) {
|
|
195
|
-
throw new Error('sink already called on stream')
|
|
196
|
-
}
|
|
197
|
-
sinkSunk = true
|
|
198
|
-
try {
|
|
199
|
-
this.writeStatus = 'writing'
|
|
200
|
-
|
|
201
|
-
for await (const chunks of source) {
|
|
202
|
-
if (chunks instanceof Uint8Array) {
|
|
203
|
-
await writer.write(chunks)
|
|
204
|
-
} else {
|
|
205
|
-
for (const buf of chunks) {
|
|
206
|
-
await writer.write(buf)
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
this.writeStatus = 'done'
|
|
212
|
-
} finally {
|
|
213
|
-
this.timeline.closeWrite = Date.now()
|
|
214
|
-
this.writeStatus = 'closed'
|
|
215
|
-
|
|
216
|
-
await stream.closeWrite()
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
return stream
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function parseMultiaddr (ma: Multiaddr): { url: string, certhashes: MultihashDigest[], remotePeer?: PeerId } {
|
|
225
|
-
const parts = ma.stringTuples()
|
|
226
|
-
|
|
227
|
-
// This is simpler to have inline than extract into a separate function
|
|
228
|
-
// eslint-disable-next-line complexity
|
|
229
|
-
const { url, certhashes, remotePeer } = parts.reduce((state: { url: string, certhashes: MultihashDigest[], seenHost: boolean, seenPort: boolean, remotePeer?: PeerId }, [proto, value]) => {
|
|
230
|
-
switch (proto) {
|
|
231
|
-
case protocols('ip6').code:
|
|
232
|
-
// @ts-expect-error - ts error on switch fallthrough
|
|
233
|
-
case protocols('dns6').code:
|
|
234
|
-
if (value?.includes(':') === true) {
|
|
235
|
-
/**
|
|
236
|
-
* This resolves cases where `new globalThis.WebTransport` fails to construct because of an invalid URL being passed.
|
|
237
|
-
*
|
|
238
|
-
* `new URL('https://::1:4001/blah')` will throw a `TypeError: Failed to construct 'URL': Invalid URL`
|
|
239
|
-
* `new URL('https://[::1]:4001/blah')` is valid and will not.
|
|
240
|
-
*
|
|
241
|
-
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2
|
|
242
|
-
*/
|
|
243
|
-
value = `[${value}]`
|
|
244
|
-
}
|
|
245
|
-
// eslint-disable-next-line no-fallthrough
|
|
246
|
-
case protocols('ip4').code:
|
|
247
|
-
case protocols('dns4').code:
|
|
248
|
-
if (state.seenHost || state.seenPort) {
|
|
249
|
-
throw new Error('Invalid multiaddr, saw host and already saw the host or port')
|
|
250
|
-
}
|
|
251
|
-
return {
|
|
252
|
-
...state,
|
|
253
|
-
url: `${state.url}${value ?? ''}`,
|
|
254
|
-
seenHost: true
|
|
255
|
-
}
|
|
256
|
-
case protocols('quic').code:
|
|
257
|
-
case protocols('quic-v1').code:
|
|
258
|
-
case protocols('webtransport').code:
|
|
259
|
-
if (!state.seenHost || !state.seenPort) {
|
|
260
|
-
throw new Error("Invalid multiaddr, Didn't see host and port, but saw quic/webtransport")
|
|
261
|
-
}
|
|
262
|
-
return state
|
|
263
|
-
case protocols('udp').code:
|
|
264
|
-
if (state.seenPort) {
|
|
265
|
-
throw new Error('Invalid multiaddr, saw port but already saw the port')
|
|
266
|
-
}
|
|
267
|
-
return {
|
|
268
|
-
...state,
|
|
269
|
-
url: `${state.url}:${value ?? ''}`,
|
|
270
|
-
seenPort: true
|
|
271
|
-
}
|
|
272
|
-
case protocols('certhash').code:
|
|
273
|
-
if (!state.seenHost || !state.seenPort) {
|
|
274
|
-
throw new Error('Invalid multiaddr, saw the certhash before seeing the host and port')
|
|
275
|
-
}
|
|
276
|
-
return {
|
|
277
|
-
...state,
|
|
278
|
-
certhashes: state.certhashes.concat([decodeCerthashStr(value ?? '')])
|
|
279
|
-
}
|
|
280
|
-
case protocols('p2p').code:
|
|
281
|
-
return {
|
|
282
|
-
...state,
|
|
283
|
-
remotePeer: peerIdFromString(value ?? '')
|
|
284
|
-
}
|
|
285
|
-
default:
|
|
286
|
-
throw new Error(`unexpected component in multiaddr: ${proto} ${protocols(proto).name} ${value ?? ''} `)
|
|
287
|
-
}
|
|
288
|
-
},
|
|
289
|
-
// All webtransport urls are https
|
|
290
|
-
{ url: 'https://', seenHost: false, seenPort: false, certhashes: [] })
|
|
291
|
-
|
|
292
|
-
return { url, certhashes, remotePeer }
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Determines if `maybeSubset` is a subset of `set`. This means that all byte arrays in `maybeSubset` are present in `set`.
|
|
296
|
-
export function isSubset (set: Uint8Array[], maybeSubset: Uint8Array[]): boolean {
|
|
297
|
-
const intersection = maybeSubset.filter(byteArray => {
|
|
298
|
-
return Boolean(set.find((otherByteArray: Uint8Array) => {
|
|
299
|
-
if (byteArray.length !== otherByteArray.length) {
|
|
300
|
-
return false
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
for (let index = 0; index < byteArray.length; index++) {
|
|
304
|
-
if (otherByteArray[index] !== byteArray[index]) {
|
|
305
|
-
return false
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
return true
|
|
309
|
-
}))
|
|
310
|
-
})
|
|
311
|
-
return (intersection.length === maybeSubset.length)
|
|
312
|
-
}
|
|
29
|
+
const log = logger('libp2p:webtransport')
|
|
313
30
|
|
|
314
31
|
export interface WebTransportInit {
|
|
315
32
|
maxInboundStreams?: number
|
|
@@ -335,6 +52,8 @@ class WebTransportTransport implements Transport {
|
|
|
335
52
|
readonly [symbol] = true
|
|
336
53
|
|
|
337
54
|
async dial (ma: Multiaddr, options: DialOptions): Promise<Connection> {
|
|
55
|
+
options?.signal?.throwIfAborted()
|
|
56
|
+
|
|
338
57
|
log('dialing %s', ma)
|
|
339
58
|
const localPeer = this.components.peerId
|
|
340
59
|
if (localPeer === undefined) {
|
|
@@ -345,60 +64,122 @@ class WebTransportTransport implements Transport {
|
|
|
345
64
|
|
|
346
65
|
const { url, certhashes, remotePeer } = parseMultiaddr(ma)
|
|
347
66
|
|
|
348
|
-
if (certhashes.length === 0) {
|
|
349
|
-
throw new Error('Expected multiaddr to contain certhashes')
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
const wt = new WebTransport(`${url}/.well-known/libp2p-webtransport?type=noise`, {
|
|
353
|
-
serverCertificateHashes: certhashes.map(certhash => ({
|
|
354
|
-
algorithm: 'sha-256',
|
|
355
|
-
value: certhash.digest
|
|
356
|
-
}))
|
|
357
|
-
})
|
|
358
|
-
wt.closed.catch((error: Error) => {
|
|
359
|
-
log.error('WebTransport transport closed due to:', error)
|
|
360
|
-
})
|
|
361
|
-
await wt.ready
|
|
362
|
-
|
|
363
67
|
if (remotePeer == null) {
|
|
364
68
|
throw new Error('Need a target peerid')
|
|
365
69
|
}
|
|
366
70
|
|
|
367
|
-
if (
|
|
368
|
-
throw new Error('
|
|
71
|
+
if (certhashes.length === 0) {
|
|
72
|
+
throw new Error('Expected multiaddr to contain certhashes')
|
|
369
73
|
}
|
|
370
74
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
log('Closing webtransport')
|
|
374
|
-
await wt.close()
|
|
375
|
-
},
|
|
376
|
-
abort: (err: Error) => {
|
|
377
|
-
log('Aborting webtransport with err:', err)
|
|
378
|
-
wt.close()
|
|
379
|
-
},
|
|
380
|
-
remoteAddr: ma,
|
|
381
|
-
timeline: {
|
|
382
|
-
open: Date.now()
|
|
383
|
-
},
|
|
384
|
-
// This connection is never used directly since webtransport supports native streams.
|
|
385
|
-
...inertDuplex()
|
|
386
|
-
}
|
|
75
|
+
let abortListener: (() => void) | undefined
|
|
76
|
+
let maConn: MultiaddrConnection | undefined
|
|
387
77
|
|
|
388
|
-
|
|
389
|
-
log.error('WebTransport connection closed:', err)
|
|
390
|
-
// This is how we specify the connection is closed and shouldn't be used.
|
|
391
|
-
maConn.timeline.close = Date.now()
|
|
392
|
-
})
|
|
78
|
+
let cleanUpWTSession: (closeInfo?: WebTransportCloseInfo) => void = () => {}
|
|
393
79
|
|
|
394
80
|
try {
|
|
81
|
+
let closed = false
|
|
82
|
+
const wt = new WebTransport(`${url}/.well-known/libp2p-webtransport?type=noise`, {
|
|
83
|
+
serverCertificateHashes: certhashes.map(certhash => ({
|
|
84
|
+
algorithm: 'sha-256',
|
|
85
|
+
value: certhash.digest
|
|
86
|
+
}))
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
cleanUpWTSession = (closeInfo?: WebTransportCloseInfo) => {
|
|
90
|
+
try {
|
|
91
|
+
if (maConn != null) {
|
|
92
|
+
if (maConn.timeline.close != null) {
|
|
93
|
+
// already closed session
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// This is how we specify the connection is closed and shouldn't be used.
|
|
98
|
+
maConn.timeline.close = Date.now()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (closed) {
|
|
102
|
+
// already closed session
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
wt.close(closeInfo)
|
|
107
|
+
} catch (err) {
|
|
108
|
+
log.error('error closing wt session', err)
|
|
109
|
+
} finally {
|
|
110
|
+
closed = true
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// this promise resolves/throws when the session is closed or failed to init
|
|
115
|
+
wt.closed
|
|
116
|
+
.then(async () => {
|
|
117
|
+
await maConn?.close()
|
|
118
|
+
})
|
|
119
|
+
.catch((err: Error) => {
|
|
120
|
+
log.error('error on remote wt session close', err)
|
|
121
|
+
maConn?.abort(err)
|
|
122
|
+
})
|
|
123
|
+
.finally(() => {
|
|
124
|
+
// if we never got as far as creating the maConn, just clean up the session
|
|
125
|
+
if (maConn == null) {
|
|
126
|
+
cleanUpWTSession()
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
// if the dial is aborted before we are ready, close the WebTransport session
|
|
131
|
+
abortListener = () => {
|
|
132
|
+
if (abortListener != null) {
|
|
133
|
+
options.signal?.removeEventListener('abort', abortListener)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
cleanUpWTSession()
|
|
137
|
+
}
|
|
138
|
+
options.signal?.addEventListener('abort', abortListener)
|
|
139
|
+
|
|
140
|
+
await wt.ready
|
|
141
|
+
|
|
142
|
+
if (!await this.authenticateWebTransport(wt, localPeer, remotePeer, certhashes)) {
|
|
143
|
+
throw new Error('Failed to authenticate webtransport')
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
maConn = {
|
|
147
|
+
close: async (options?: AbortOptions) => {
|
|
148
|
+
log('Closing webtransport')
|
|
149
|
+
cleanUpWTSession()
|
|
150
|
+
},
|
|
151
|
+
abort: (err: Error) => {
|
|
152
|
+
log('aborting webtransport due to passed err', err)
|
|
153
|
+
cleanUpWTSession({
|
|
154
|
+
closeCode: 0,
|
|
155
|
+
reason: err.message
|
|
156
|
+
})
|
|
157
|
+
},
|
|
158
|
+
remoteAddr: ma,
|
|
159
|
+
timeline: {
|
|
160
|
+
open: Date.now()
|
|
161
|
+
},
|
|
162
|
+
// This connection is never used directly since webtransport supports native streams.
|
|
163
|
+
...inertDuplex()
|
|
164
|
+
}
|
|
165
|
+
|
|
395
166
|
options?.signal?.throwIfAborted()
|
|
396
|
-
} catch (e) {
|
|
397
|
-
wt.close()
|
|
398
|
-
throw e
|
|
399
|
-
}
|
|
400
167
|
|
|
401
|
-
|
|
168
|
+
return await options.upgrader.upgradeOutbound(maConn, { skipEncryption: true, muxerFactory: this.webtransportMuxer(wt, cleanUpWTSession), skipProtection: true })
|
|
169
|
+
} catch (err: any) {
|
|
170
|
+
log.error('caught wt session err', err)
|
|
171
|
+
|
|
172
|
+
cleanUpWTSession({
|
|
173
|
+
closeCode: 0,
|
|
174
|
+
reason: err.message
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
throw err
|
|
178
|
+
} finally {
|
|
179
|
+
if (abortListener != null) {
|
|
180
|
+
options.signal?.removeEventListener('abort', abortListener)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
402
183
|
}
|
|
403
184
|
|
|
404
185
|
async authenticateWebTransport (wt: InstanceType<typeof WebTransport>, localPeer: PeerId, remotePeer: PeerId, certhashes: Array<MultihashDigest<number>>): Promise<boolean> {
|
|
@@ -449,7 +230,7 @@ class WebTransportTransport implements Transport {
|
|
|
449
230
|
return true
|
|
450
231
|
}
|
|
451
232
|
|
|
452
|
-
webtransportMuxer (wt: InstanceType<typeof WebTransport
|
|
233
|
+
webtransportMuxer (wt: InstanceType<typeof WebTransport>, cleanUpWTSession: WebTransportSessionCleanup): StreamMuxerFactory {
|
|
453
234
|
let streamIDCounter = 0
|
|
454
235
|
const config = this.config
|
|
455
236
|
return {
|
|
@@ -510,11 +291,14 @@ class WebTransportTransport implements Transport {
|
|
|
510
291
|
*/
|
|
511
292
|
close: async (options?: AbortOptions) => {
|
|
512
293
|
log('Closing webtransport muxer')
|
|
513
|
-
|
|
294
|
+
cleanUpWTSession()
|
|
514
295
|
},
|
|
515
296
|
abort: (err: Error) => {
|
|
516
297
|
log('Aborting webtransport muxer with err:', err)
|
|
517
|
-
|
|
298
|
+
cleanUpWTSession({
|
|
299
|
+
closeCode: 0,
|
|
300
|
+
reason: err.message
|
|
301
|
+
})
|
|
518
302
|
},
|
|
519
303
|
// This stream muxer is webtransport native. Therefore it doesn't plug in with any other duplex.
|
|
520
304
|
...inertDuplex()
|
package/src/stream.ts
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { logger } from '@libp2p/logger'
|
|
2
|
+
import { Uint8ArrayList } from 'uint8arraylist'
|
|
3
|
+
import type { AbortOptions } from '@libp2p/interface'
|
|
4
|
+
import type { Direction, Stream } from '@libp2p/interface/connection'
|
|
5
|
+
import type { Source } from 'it-stream-types'
|
|
6
|
+
|
|
7
|
+
const log = logger('libp2p:webtransport:stream')
|
|
8
|
+
|
|
9
|
+
export async function webtransportBiDiStreamToStream (bidiStream: any, streamId: string, direction: Direction, activeStreams: Stream[], onStreamEnd: undefined | ((s: Stream) => void)): Promise<Stream> {
|
|
10
|
+
const writer = bidiStream.writable.getWriter()
|
|
11
|
+
const reader = bidiStream.readable.getReader()
|
|
12
|
+
await writer.ready
|
|
13
|
+
|
|
14
|
+
function cleanupStreamFromActiveStreams (): void {
|
|
15
|
+
const index = activeStreams.findIndex(s => s === stream)
|
|
16
|
+
if (index !== -1) {
|
|
17
|
+
activeStreams.splice(index, 1)
|
|
18
|
+
stream.timeline.close = Date.now()
|
|
19
|
+
onStreamEnd?.(stream)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let writerClosed = false
|
|
24
|
+
let readerClosed = false;
|
|
25
|
+
(async function () {
|
|
26
|
+
const err: Error | undefined = await writer.closed.catch((err: Error) => err)
|
|
27
|
+
if (err != null) {
|
|
28
|
+
const msg = err.message
|
|
29
|
+
if (!(msg.includes('aborted by the remote server') || msg.includes('STOP_SENDING'))) {
|
|
30
|
+
log.error(`WebTransport writer closed unexpectedly: streamId=${streamId} err=${err.message}`)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
writerClosed = true
|
|
34
|
+
if (writerClosed && readerClosed) {
|
|
35
|
+
cleanupStreamFromActiveStreams()
|
|
36
|
+
}
|
|
37
|
+
})().catch(() => {
|
|
38
|
+
log.error('WebTransport failed to cleanup closed stream')
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
(async function () {
|
|
42
|
+
const err: Error | undefined = await reader.closed.catch((err: Error) => err)
|
|
43
|
+
if (err != null) {
|
|
44
|
+
log.error(`WebTransport reader closed unexpectedly: streamId=${streamId} err=${err.message}`)
|
|
45
|
+
}
|
|
46
|
+
readerClosed = true
|
|
47
|
+
if (writerClosed && readerClosed) {
|
|
48
|
+
cleanupStreamFromActiveStreams()
|
|
49
|
+
}
|
|
50
|
+
})().catch(() => {
|
|
51
|
+
log.error('WebTransport failed to cleanup closed stream')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
let sinkSunk = false
|
|
55
|
+
const stream: Stream = {
|
|
56
|
+
id: streamId,
|
|
57
|
+
status: 'open',
|
|
58
|
+
writeStatus: 'ready',
|
|
59
|
+
readStatus: 'ready',
|
|
60
|
+
abort (err: Error) {
|
|
61
|
+
if (!writerClosed) {
|
|
62
|
+
writer.abort(err)
|
|
63
|
+
writerClosed = true
|
|
64
|
+
}
|
|
65
|
+
readerClosed = true
|
|
66
|
+
|
|
67
|
+
this.status = 'aborted'
|
|
68
|
+
this.writeStatus = 'closed'
|
|
69
|
+
this.readStatus = 'closed'
|
|
70
|
+
|
|
71
|
+
this.timeline.reset =
|
|
72
|
+
this.timeline.close =
|
|
73
|
+
this.timeline.closeRead =
|
|
74
|
+
this.timeline.closeWrite = Date.now()
|
|
75
|
+
|
|
76
|
+
cleanupStreamFromActiveStreams()
|
|
77
|
+
},
|
|
78
|
+
async close (options?: AbortOptions) {
|
|
79
|
+
this.status = 'closing'
|
|
80
|
+
|
|
81
|
+
await Promise.all([
|
|
82
|
+
stream.closeRead(options),
|
|
83
|
+
stream.closeWrite(options)
|
|
84
|
+
])
|
|
85
|
+
|
|
86
|
+
cleanupStreamFromActiveStreams()
|
|
87
|
+
|
|
88
|
+
this.status = 'closed'
|
|
89
|
+
this.timeline.close = Date.now()
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
async closeRead (options?: AbortOptions) {
|
|
93
|
+
if (!readerClosed) {
|
|
94
|
+
this.readStatus = 'closing'
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
await reader.cancel()
|
|
98
|
+
} catch (err: any) {
|
|
99
|
+
if (err.toString().includes('RESET_STREAM') === true) {
|
|
100
|
+
writerClosed = true
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.timeline.closeRead = Date.now()
|
|
105
|
+
this.readStatus = 'closed'
|
|
106
|
+
|
|
107
|
+
readerClosed = true
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (writerClosed) {
|
|
111
|
+
cleanupStreamFromActiveStreams()
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
async closeWrite (options?: AbortOptions) {
|
|
116
|
+
if (!writerClosed) {
|
|
117
|
+
writerClosed = true
|
|
118
|
+
|
|
119
|
+
this.writeStatus = 'closing'
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
await writer.close()
|
|
123
|
+
} catch (err: any) {
|
|
124
|
+
if (err.toString().includes('RESET_STREAM') === true) {
|
|
125
|
+
readerClosed = true
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this.timeline.closeWrite = Date.now()
|
|
130
|
+
this.writeStatus = 'closed'
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (readerClosed) {
|
|
134
|
+
cleanupStreamFromActiveStreams()
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
direction,
|
|
138
|
+
timeline: { open: Date.now() },
|
|
139
|
+
metadata: {},
|
|
140
|
+
source: (async function * () {
|
|
141
|
+
while (true) {
|
|
142
|
+
const val = await reader.read()
|
|
143
|
+
if (val.done === true) {
|
|
144
|
+
readerClosed = true
|
|
145
|
+
if (writerClosed) {
|
|
146
|
+
cleanupStreamFromActiveStreams()
|
|
147
|
+
}
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
yield new Uint8ArrayList(val.value)
|
|
152
|
+
}
|
|
153
|
+
})(),
|
|
154
|
+
sink: async function (source: Source<Uint8Array | Uint8ArrayList>) {
|
|
155
|
+
if (sinkSunk) {
|
|
156
|
+
throw new Error('sink already called on stream')
|
|
157
|
+
}
|
|
158
|
+
sinkSunk = true
|
|
159
|
+
try {
|
|
160
|
+
this.writeStatus = 'writing'
|
|
161
|
+
|
|
162
|
+
for await (const chunks of source) {
|
|
163
|
+
if (chunks instanceof Uint8Array) {
|
|
164
|
+
await writer.write(chunks)
|
|
165
|
+
} else {
|
|
166
|
+
for (const buf of chunks) {
|
|
167
|
+
await writer.write(buf)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
this.writeStatus = 'done'
|
|
173
|
+
} finally {
|
|
174
|
+
this.timeline.closeWrite = Date.now()
|
|
175
|
+
this.writeStatus = 'closed'
|
|
176
|
+
|
|
177
|
+
await stream.closeWrite()
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return stream
|
|
183
|
+
}
|