@libp2p/webtransport 3.0.10 → 3.0.11-6cb80f7d
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 +9 -9
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +65 -53
- package/dist/src/index.js.map +1 -1
- package/dist/src/utils/parse-multiaddr.d.ts +3 -2
- package/dist/src/utils/parse-multiaddr.d.ts.map +1 -1
- package/dist/src/utils/parse-multiaddr.js +31 -65
- package/dist/src/utils/parse-multiaddr.js.map +1 -1
- package/package.json +6 -5
- package/src/index.ts +81 -59
- package/src/utils/parse-multiaddr.ts +41 -66
- package/dist/typedoc-urls.json +0 -8
package/src/index.ts
CHANGED
|
@@ -7,13 +7,14 @@ import { inertDuplex } from './utils/inert-duplex.js'
|
|
|
7
7
|
import { isSubset } from './utils/is-subset.js'
|
|
8
8
|
import { parseMultiaddr } from './utils/parse-multiaddr.js'
|
|
9
9
|
import type { Connection, MultiaddrConnection, Stream } from '@libp2p/interface/connection'
|
|
10
|
+
import type { CounterGroup, Metrics } from '@libp2p/interface/metrics'
|
|
10
11
|
import type { PeerId } from '@libp2p/interface/peer-id'
|
|
11
12
|
import type { StreamMuxerFactory, StreamMuxerInit, StreamMuxer } from '@libp2p/interface/stream-muxer'
|
|
12
13
|
import type { Source } from 'it-stream-types'
|
|
13
14
|
import type { MultihashDigest } from 'multiformats/hashes/interface'
|
|
14
15
|
|
|
15
16
|
interface WebTransportSessionCleanup {
|
|
16
|
-
(
|
|
17
|
+
(metric: string): void
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
const log = logger('libp2p:webtransport')
|
|
@@ -24,17 +25,32 @@ export interface WebTransportInit {
|
|
|
24
25
|
|
|
25
26
|
export interface WebTransportComponents {
|
|
26
27
|
peerId: PeerId
|
|
28
|
+
metrics?: Metrics
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface WebTransportMetrics {
|
|
32
|
+
dialerEvents: CounterGroup
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
class WebTransportTransport implements Transport {
|
|
30
36
|
private readonly components: WebTransportComponents
|
|
31
37
|
private readonly config: Required<WebTransportInit>
|
|
38
|
+
private readonly metrics?: WebTransportMetrics
|
|
32
39
|
|
|
33
40
|
constructor (components: WebTransportComponents, init: WebTransportInit = {}) {
|
|
34
41
|
this.components = components
|
|
35
42
|
this.config = {
|
|
36
43
|
maxInboundStreams: init.maxInboundStreams ?? 1000
|
|
37
44
|
}
|
|
45
|
+
|
|
46
|
+
if (components.metrics != null) {
|
|
47
|
+
this.metrics = {
|
|
48
|
+
dialerEvents: components.metrics.registerCounterGroup('libp2p_webtransport_dialer_events_total', {
|
|
49
|
+
label: 'event',
|
|
50
|
+
help: 'Total count of WebTransport dialer events by type'
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
}
|
|
38
54
|
}
|
|
39
55
|
|
|
40
56
|
readonly [Symbol.toStringTag] = '@libp2p/webtransport'
|
|
@@ -65,10 +81,15 @@ class WebTransportTransport implements Transport {
|
|
|
65
81
|
let abortListener: (() => void) | undefined
|
|
66
82
|
let maConn: MultiaddrConnection | undefined
|
|
67
83
|
|
|
68
|
-
let cleanUpWTSession:
|
|
84
|
+
let cleanUpWTSession: WebTransportSessionCleanup = () => {}
|
|
85
|
+
|
|
86
|
+
let closed = false
|
|
87
|
+
let ready = false
|
|
88
|
+
let authenticated = false
|
|
69
89
|
|
|
70
90
|
try {
|
|
71
|
-
|
|
91
|
+
this.metrics?.dialerEvents.increment({ pending: true })
|
|
92
|
+
|
|
72
93
|
const wt = new WebTransport(`${url}/.well-known/libp2p-webtransport?type=noise`, {
|
|
73
94
|
serverCertificateHashes: certhashes.map(certhash => ({
|
|
74
95
|
algorithm: 'sha-256',
|
|
@@ -76,74 +97,69 @@ class WebTransportTransport implements Transport {
|
|
|
76
97
|
}))
|
|
77
98
|
})
|
|
78
99
|
|
|
79
|
-
cleanUpWTSession = (
|
|
100
|
+
cleanUpWTSession = (metric: string) => {
|
|
101
|
+
if (closed) {
|
|
102
|
+
// already closed session
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
80
106
|
try {
|
|
107
|
+
this.metrics?.dialerEvents.increment({ [metric]: true })
|
|
108
|
+
wt.close()
|
|
109
|
+
} catch (err) {
|
|
110
|
+
log.error('error closing wt session', err)
|
|
111
|
+
} finally {
|
|
112
|
+
// This is how we specify the connection is closed and shouldn't be used.
|
|
81
113
|
if (maConn != null) {
|
|
82
|
-
if (maConn.timeline.close != null) {
|
|
83
|
-
// already closed session
|
|
84
|
-
return
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// This is how we specify the connection is closed and shouldn't be used.
|
|
88
114
|
maConn.timeline.close = Date.now()
|
|
89
115
|
}
|
|
90
116
|
|
|
91
|
-
if (closed) {
|
|
92
|
-
// already closed session
|
|
93
|
-
return
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
wt.close(closeInfo)
|
|
97
|
-
} catch (err) {
|
|
98
|
-
log.error('error closing wt session', err)
|
|
99
|
-
} finally {
|
|
100
117
|
closed = true
|
|
101
118
|
}
|
|
102
119
|
}
|
|
103
120
|
|
|
104
|
-
// this promise resolves/throws when the session is closed or failed to init
|
|
105
|
-
wt.closed
|
|
106
|
-
.then(async () => {
|
|
107
|
-
await maConn?.close()
|
|
108
|
-
})
|
|
109
|
-
.catch((err: Error) => {
|
|
110
|
-
log.error('error on remote wt session close', err)
|
|
111
|
-
maConn?.abort(err)
|
|
112
|
-
})
|
|
113
|
-
.finally(() => {
|
|
114
|
-
// if we never got as far as creating the maConn, just clean up the session
|
|
115
|
-
if (maConn == null) {
|
|
116
|
-
cleanUpWTSession()
|
|
117
|
-
}
|
|
118
|
-
})
|
|
119
|
-
|
|
120
121
|
// if the dial is aborted before we are ready, close the WebTransport session
|
|
121
122
|
abortListener = () => {
|
|
122
|
-
if (
|
|
123
|
-
|
|
123
|
+
if (ready) {
|
|
124
|
+
cleanUpWTSession('noise_timeout')
|
|
125
|
+
} else {
|
|
126
|
+
cleanUpWTSession('ready_timeout')
|
|
124
127
|
}
|
|
125
|
-
|
|
126
|
-
cleanUpWTSession()
|
|
127
128
|
}
|
|
128
|
-
options.signal?.addEventListener('abort', abortListener
|
|
129
|
+
options.signal?.addEventListener('abort', abortListener, {
|
|
130
|
+
once: true
|
|
131
|
+
})
|
|
129
132
|
|
|
130
|
-
await
|
|
133
|
+
await Promise.race([
|
|
134
|
+
wt.closed,
|
|
135
|
+
wt.ready
|
|
136
|
+
])
|
|
137
|
+
|
|
138
|
+
ready = true
|
|
139
|
+
this.metrics?.dialerEvents.increment({ ready: true })
|
|
140
|
+
|
|
141
|
+
// this promise resolves/throws when the session is closed
|
|
142
|
+
wt.closed.catch((err: Error) => {
|
|
143
|
+
log.error('error on remote wt session close', err)
|
|
144
|
+
})
|
|
145
|
+
.finally(() => {
|
|
146
|
+
cleanUpWTSession('remote_close')
|
|
147
|
+
})
|
|
131
148
|
|
|
132
149
|
if (!await this.authenticateWebTransport(wt, localPeer, remotePeer, certhashes)) {
|
|
133
150
|
throw new Error('Failed to authenticate webtransport')
|
|
134
151
|
}
|
|
135
152
|
|
|
153
|
+
this.metrics?.dialerEvents.increment({ open: true })
|
|
154
|
+
|
|
136
155
|
maConn = {
|
|
137
|
-
close: async (
|
|
156
|
+
close: async () => {
|
|
138
157
|
log('Closing webtransport')
|
|
139
|
-
cleanUpWTSession()
|
|
158
|
+
cleanUpWTSession('close')
|
|
140
159
|
},
|
|
141
160
|
abort: (err: Error) => {
|
|
142
161
|
log('aborting webtransport due to passed err', err)
|
|
143
|
-
cleanUpWTSession(
|
|
144
|
-
closeCode: 0,
|
|
145
|
-
reason: err.message
|
|
146
|
-
})
|
|
162
|
+
cleanUpWTSession('abort')
|
|
147
163
|
},
|
|
148
164
|
remoteAddr: ma,
|
|
149
165
|
timeline: {
|
|
@@ -153,16 +169,19 @@ class WebTransportTransport implements Transport {
|
|
|
153
169
|
...inertDuplex()
|
|
154
170
|
}
|
|
155
171
|
|
|
156
|
-
|
|
172
|
+
authenticated = true
|
|
157
173
|
|
|
158
|
-
return await options.upgrader.upgradeOutbound(maConn, { skipEncryption: true, muxerFactory: this.webtransportMuxer(wt
|
|
174
|
+
return await options.upgrader.upgradeOutbound(maConn, { skipEncryption: true, muxerFactory: this.webtransportMuxer(wt), skipProtection: true })
|
|
159
175
|
} catch (err: any) {
|
|
160
176
|
log.error('caught wt session err', err)
|
|
161
177
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
178
|
+
if (authenticated) {
|
|
179
|
+
cleanUpWTSession('upgrade_error')
|
|
180
|
+
} else if (ready) {
|
|
181
|
+
cleanUpWTSession('noise_error')
|
|
182
|
+
} else {
|
|
183
|
+
cleanUpWTSession('ready_error')
|
|
184
|
+
}
|
|
166
185
|
|
|
167
186
|
throw err
|
|
168
187
|
} finally {
|
|
@@ -220,7 +239,7 @@ class WebTransportTransport implements Transport {
|
|
|
220
239
|
return true
|
|
221
240
|
}
|
|
222
241
|
|
|
223
|
-
webtransportMuxer (wt: WebTransport
|
|
242
|
+
webtransportMuxer (wt: WebTransport): StreamMuxerFactory {
|
|
224
243
|
let streamIDCounter = 0
|
|
225
244
|
const config = this.config
|
|
226
245
|
return {
|
|
@@ -281,14 +300,17 @@ class WebTransportTransport implements Transport {
|
|
|
281
300
|
*/
|
|
282
301
|
close: async (options?: AbortOptions) => {
|
|
283
302
|
log('Closing webtransport muxer')
|
|
284
|
-
|
|
303
|
+
|
|
304
|
+
await Promise.all(
|
|
305
|
+
activeStreams.map(async s => s.close(options))
|
|
306
|
+
)
|
|
285
307
|
},
|
|
286
308
|
abort: (err: Error) => {
|
|
287
309
|
log('Aborting webtransport muxer with err:', err)
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
310
|
+
|
|
311
|
+
for (const stream of activeStreams) {
|
|
312
|
+
stream.abort(err)
|
|
313
|
+
}
|
|
292
314
|
},
|
|
293
315
|
// This stream muxer is webtransport native. Therefore it doesn't plug in with any other duplex.
|
|
294
316
|
...inertDuplex()
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { CodeError } from '@libp2p/interface/errors'
|
|
1
2
|
import { peerIdFromString } from '@libp2p/peer-id'
|
|
2
3
|
import { type Multiaddr, protocols } from '@multiformats/multiaddr'
|
|
4
|
+
import { WebTransport } from '@multiformats/multiaddr-matcher'
|
|
3
5
|
import { bases, digest } from 'multiformats/basics'
|
|
4
6
|
import type { PeerId } from '@libp2p/interface/peer-id'
|
|
5
7
|
import type { MultihashDigest } from 'multiformats/hashes/interface'
|
|
@@ -11,73 +13,46 @@ function decodeCerthashStr (s: string): MultihashDigest {
|
|
|
11
13
|
return digest.decode(multibaseDecoder.decode(s))
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
export
|
|
16
|
+
export interface ParsedMultiaddr {
|
|
17
|
+
url: string
|
|
18
|
+
certhashes: MultihashDigest[]
|
|
19
|
+
remotePeer?: PeerId
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function parseMultiaddr (ma: Multiaddr): ParsedMultiaddr {
|
|
23
|
+
if (!WebTransport.matches(ma)) {
|
|
24
|
+
throw new CodeError('Invalid multiaddr, was not a WebTransport address', 'ERR_INVALID_MULTIADDR')
|
|
25
|
+
}
|
|
26
|
+
|
|
15
27
|
const parts = ma.stringTuples()
|
|
28
|
+
const certhashes = parts
|
|
29
|
+
.filter(([name, _]) => name === protocols('certhash').code)
|
|
30
|
+
.map(([_, value]) => decodeCerthashStr(value ?? ''))
|
|
31
|
+
|
|
32
|
+
// only take the first peer id in the multiaddr as it may be a relay
|
|
33
|
+
const remotePeer = parts
|
|
34
|
+
.filter(([name, _]) => name === protocols('p2p').code)
|
|
35
|
+
.map(([_, value]) => peerIdFromString(value ?? ''))[0]
|
|
36
|
+
|
|
37
|
+
const opts = ma.toOptions()
|
|
38
|
+
let host = opts.host
|
|
16
39
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
* `new URL('https://::1:4001/blah')` will throw a `TypeError: Failed to construct 'URL': Invalid URL`
|
|
29
|
-
* `new URL('https://[::1]:4001/blah')` is valid and will not.
|
|
30
|
-
*
|
|
31
|
-
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2
|
|
32
|
-
*/
|
|
33
|
-
value = `[${value}]`
|
|
34
|
-
}
|
|
35
|
-
// eslint-disable-next-line no-fallthrough
|
|
36
|
-
case protocols('ip4').code:
|
|
37
|
-
case protocols('dns4').code:
|
|
38
|
-
if (state.seenHost || state.seenPort) {
|
|
39
|
-
throw new Error('Invalid multiaddr, saw host and already saw the host or port')
|
|
40
|
-
}
|
|
41
|
-
return {
|
|
42
|
-
...state,
|
|
43
|
-
url: `${state.url}${value ?? ''}`,
|
|
44
|
-
seenHost: true
|
|
45
|
-
}
|
|
46
|
-
case protocols('quic').code:
|
|
47
|
-
case protocols('quic-v1').code:
|
|
48
|
-
case protocols('webtransport').code:
|
|
49
|
-
if (!state.seenHost || !state.seenPort) {
|
|
50
|
-
throw new Error("Invalid multiaddr, Didn't see host and port, but saw quic/webtransport")
|
|
51
|
-
}
|
|
52
|
-
return state
|
|
53
|
-
case protocols('udp').code:
|
|
54
|
-
if (state.seenPort) {
|
|
55
|
-
throw new Error('Invalid multiaddr, saw port but already saw the port')
|
|
56
|
-
}
|
|
57
|
-
return {
|
|
58
|
-
...state,
|
|
59
|
-
url: `${state.url}:${value ?? ''}`,
|
|
60
|
-
seenPort: true
|
|
61
|
-
}
|
|
62
|
-
case protocols('certhash').code:
|
|
63
|
-
if (!state.seenHost || !state.seenPort) {
|
|
64
|
-
throw new Error('Invalid multiaddr, saw the certhash before seeing the host and port')
|
|
65
|
-
}
|
|
66
|
-
return {
|
|
67
|
-
...state,
|
|
68
|
-
certhashes: state.certhashes.concat([decodeCerthashStr(value ?? '')])
|
|
69
|
-
}
|
|
70
|
-
case protocols('p2p').code:
|
|
71
|
-
return {
|
|
72
|
-
...state,
|
|
73
|
-
remotePeer: peerIdFromString(value ?? '')
|
|
74
|
-
}
|
|
75
|
-
default:
|
|
76
|
-
throw new Error(`unexpected component in multiaddr: ${proto} ${protocols(proto).name} ${value ?? ''} `)
|
|
77
|
-
}
|
|
78
|
-
},
|
|
79
|
-
// All webtransport urls are https
|
|
80
|
-
{ url: 'https://', seenHost: false, seenPort: false, certhashes: [] })
|
|
40
|
+
if (opts.family === 6 && host?.includes(':')) {
|
|
41
|
+
/**
|
|
42
|
+
* This resolves cases where `new WebTransport()` fails to construct because of an invalid URL being passed.
|
|
43
|
+
*
|
|
44
|
+
* `new URL('https://::1:4001/blah')` will throw a `TypeError: Failed to construct 'URL': Invalid URL`
|
|
45
|
+
* `new URL('https://[::1]:4001/blah')` is valid and will not.
|
|
46
|
+
*
|
|
47
|
+
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2
|
|
48
|
+
*/
|
|
49
|
+
host = `[${host}]`
|
|
50
|
+
}
|
|
81
51
|
|
|
82
|
-
return {
|
|
52
|
+
return {
|
|
53
|
+
// All webtransport urls are https
|
|
54
|
+
url: `https://${host}:${opts.port}`,
|
|
55
|
+
certhashes,
|
|
56
|
+
remotePeer
|
|
57
|
+
}
|
|
83
58
|
}
|
package/dist/typedoc-urls.json
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"WebTransportComponents": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_webtransport.WebTransportComponents.html",
|
|
3
|
-
".:WebTransportComponents": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_webtransport.WebTransportComponents.html",
|
|
4
|
-
"WebTransportInit": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_webtransport.WebTransportInit.html",
|
|
5
|
-
".:WebTransportInit": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_webtransport.WebTransportInit.html",
|
|
6
|
-
"webTransport": "https://libp2p.github.io/js-libp2p/functions/_libp2p_webtransport.webTransport.html",
|
|
7
|
-
".:webTransport": "https://libp2p.github.io/js-libp2p/functions/_libp2p_webtransport.webTransport.html"
|
|
8
|
-
}
|