@libp2p/webrtc 5.2.9 → 5.2.10-b9e32cc37
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 +54 -0
- package/dist/index.min.js +153 -18
- package/dist/src/constants.d.ts +20 -0
- package/dist/src/constants.d.ts.map +1 -1
- package/dist/src/constants.js +20 -0
- package/dist/src/constants.js.map +1 -1
- package/dist/src/index.d.ts +54 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +54 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/private-to-private/util.d.ts +2 -2
- package/dist/src/private-to-private/util.d.ts.map +1 -1
- package/dist/src/private-to-public/listener.d.ts +9 -4
- package/dist/src/private-to-public/listener.d.ts.map +1 -1
- package/dist/src/private-to-public/listener.js +8 -18
- package/dist/src/private-to-public/listener.js.map +1 -1
- package/dist/src/private-to-public/transport.d.ts +68 -10
- package/dist/src/private-to-public/transport.d.ts.map +1 -1
- package/dist/src/private-to-public/transport.js +204 -33
- package/dist/src/private-to-public/transport.js.map +1 -1
- package/dist/src/private-to-public/utils/connect.js +126 -124
- package/dist/src/private-to-public/utils/connect.js.map +1 -1
- package/dist/src/private-to-public/utils/pem.d.ts +6 -0
- package/dist/src/private-to-public/utils/pem.d.ts.map +1 -0
- package/dist/src/private-to-public/utils/pem.js +15 -0
- package/dist/src/private-to-public/utils/pem.js.map +1 -0
- package/dist/src/stream.d.ts +5 -0
- package/dist/src/stream.d.ts.map +1 -1
- package/dist/src/stream.js +14 -9
- package/dist/src/stream.js.map +1 -1
- package/package.json +12 -9
- package/src/constants.ts +25 -0
- package/src/index.ts +54 -0
- package/src/private-to-private/util.ts +2 -2
- package/src/private-to-public/listener.ts +18 -26
- package/src/private-to-public/transport.ts +304 -39
- package/src/private-to-public/utils/connect.ts +139 -135
- package/src/private-to-public/utils/pem.ts +18 -0
- package/src/stream.ts +20 -11
- package/dist/typedoc-urls.json +0 -14
@@ -1,20 +1,27 @@
|
|
1
|
-
import {
|
1
|
+
import { generateKeyPair, privateKeyToCryptoKeyPair } from '@libp2p/crypto/keys'
|
2
|
+
import { InvalidParametersError, NotFoundError, NotStartedError, TypedEventEmitter, serviceCapabilities, transportSymbol } from '@libp2p/interface'
|
2
3
|
import { peerIdFromString } from '@libp2p/peer-id'
|
3
4
|
import { WebRTCDirect } from '@multiformats/multiaddr-matcher'
|
4
|
-
import {
|
5
|
+
import { BasicConstraintsExtension, X509Certificate, X509CertificateGenerator } from '@peculiar/x509'
|
6
|
+
import { Key } from 'interface-datastore'
|
7
|
+
import { base64url } from 'multiformats/bases/base64'
|
8
|
+
import { sha256 } from 'multiformats/hashes/sha2'
|
9
|
+
import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
|
10
|
+
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
|
11
|
+
import { DEFAULT_CERTIFICATE_DATASTORE_KEY, DEFAULT_CERTIFICATE_LIFESPAN, DEFAULT_CERTIFICATE_PRIVATE_KEY_NAME, DEFAULT_CERTIFICATE_RENEWAL_THRESHOLD } from '../constants.js'
|
5
12
|
import { genUfrag } from '../util.js'
|
6
13
|
import { WebRTCDirectListener } from './listener.js'
|
7
14
|
import { connect } from './utils/connect.js'
|
8
15
|
import { createDialerRTCPeerConnection } from './utils/get-rtcpeerconnection.js'
|
16
|
+
import { formatAsPem } from './utils/pem.js'
|
9
17
|
import type { DataChannelOptions, TransportCertificate } from '../index.js'
|
10
18
|
import type { WebRTCDialEvents } from '../private-to-private/transport.js'
|
11
|
-
import type { CreateListenerOptions, Transport, Listener, ComponentLogger, Logger, Connection, CounterGroup, Metrics, PeerId, DialTransportOptions, PrivateKey, Upgrader } from '@libp2p/interface'
|
19
|
+
import type { CreateListenerOptions, Transport, Listener, ComponentLogger, Logger, Connection, CounterGroup, Metrics, PeerId, DialTransportOptions, PrivateKey, Upgrader, Startable, TypedEventTarget } from '@libp2p/interface'
|
12
20
|
import type { TransportManager } from '@libp2p/interface-internal'
|
21
|
+
import type { Keychain } from '@libp2p/keychain'
|
13
22
|
import type { Multiaddr } from '@multiformats/multiaddr'
|
23
|
+
import type { Datastore } from 'interface-datastore'
|
14
24
|
|
15
|
-
/**
|
16
|
-
* The peer for this transport
|
17
|
-
*/
|
18
25
|
export interface WebRTCDirectTransportComponents {
|
19
26
|
peerId: PeerId
|
20
27
|
privateKey: PrivateKey
|
@@ -22,6 +29,8 @@ export interface WebRTCDirectTransportComponents {
|
|
22
29
|
logger: ComponentLogger
|
23
30
|
transportManager: TransportManager
|
24
31
|
upgrader: Upgrader
|
32
|
+
keychain?: Keychain
|
33
|
+
datastore: Datastore
|
25
34
|
}
|
26
35
|
|
27
36
|
export interface WebRTCMetrics {
|
@@ -29,26 +38,90 @@ export interface WebRTCMetrics {
|
|
29
38
|
}
|
30
39
|
|
31
40
|
export interface WebRTCTransportDirectInit {
|
41
|
+
/**
|
42
|
+
* The default configuration used by all created RTCPeerConnections
|
43
|
+
*/
|
32
44
|
rtcConfiguration?: RTCConfiguration | (() => RTCConfiguration | Promise<RTCConfiguration>)
|
45
|
+
|
46
|
+
/**
|
47
|
+
* The default configuration used by all created RTCDataChannels
|
48
|
+
*/
|
33
49
|
dataChannel?: DataChannelOptions
|
50
|
+
|
51
|
+
/**
|
52
|
+
* @deprecated use `certificate` instead - this option will be removed in a future release
|
53
|
+
*/
|
34
54
|
certificates?: TransportCertificate[]
|
35
55
|
|
56
|
+
/**
|
57
|
+
* Use an existing TLS certificate to secure incoming connections or supply
|
58
|
+
* settings to generate one.
|
59
|
+
*
|
60
|
+
* This must be an ECDSA certificate using the P-256 curve.
|
61
|
+
*
|
62
|
+
* From our testing we find that P-256 elliptic curve is supported by Pion,
|
63
|
+
* webrtc-rs, as well as Chromium (P-228 and P-384 was not supported in
|
64
|
+
* Chromium).
|
65
|
+
*/
|
66
|
+
certificate?: TransportCertificate
|
67
|
+
|
36
68
|
/**
|
37
69
|
* @deprecated this setting is ignored and will be removed in a future release
|
38
70
|
*/
|
39
71
|
useLibjuice?: boolean
|
72
|
+
|
73
|
+
/**
|
74
|
+
* The key the certificate is stored in the datastore under
|
75
|
+
*
|
76
|
+
* @default '/libp2p/webrtc-direct/certificate'
|
77
|
+
*/
|
78
|
+
certificateDatastoreKey?: string
|
79
|
+
|
80
|
+
/**
|
81
|
+
* The name the certificate private key is stored in the keychain with
|
82
|
+
*
|
83
|
+
* @default 'webrtc-direct-certificate-private-key'
|
84
|
+
*/
|
85
|
+
certificateKeychainName?: string
|
86
|
+
|
87
|
+
/**
|
88
|
+
* Number of ms a certificate should be valid for (defaults to 14 days)
|
89
|
+
*
|
90
|
+
* @default 2_592_000_000
|
91
|
+
*/
|
92
|
+
certificateLifespan?: number
|
93
|
+
|
94
|
+
/**
|
95
|
+
* Certificates will be renewed this many ms before expiry (defaults to 1 day)
|
96
|
+
*
|
97
|
+
* @default 86_400_000
|
98
|
+
*/
|
99
|
+
certificateRenewalThreshold?: number
|
40
100
|
}
|
41
101
|
|
42
|
-
export
|
102
|
+
export interface WebRTCDirectTransportCertificateEvents {
|
103
|
+
'certificate:renew': CustomEvent<TransportCertificate>
|
104
|
+
}
|
105
|
+
|
106
|
+
export class WebRTCDirectTransport implements Transport, Startable {
|
43
107
|
private readonly log: Logger
|
44
108
|
private readonly metrics?: WebRTCMetrics
|
45
109
|
private readonly components: WebRTCDirectTransportComponents
|
46
110
|
private readonly init: WebRTCTransportDirectInit
|
111
|
+
private certificate?: TransportCertificate
|
112
|
+
private privateKey?: PrivateKey
|
113
|
+
private readonly emitter: TypedEventTarget<WebRTCDirectTransportCertificateEvents>
|
114
|
+
private renewCertificateTask?: ReturnType<typeof setTimeout>
|
47
115
|
|
48
116
|
constructor (components: WebRTCDirectTransportComponents, init: WebRTCTransportDirectInit = {}) {
|
49
117
|
this.log = components.logger.forComponent('libp2p:webrtc-direct')
|
50
118
|
this.components = components
|
51
119
|
this.init = init
|
120
|
+
this.emitter = new TypedEventEmitter()
|
121
|
+
|
122
|
+
if (init.certificateLifespan != null && init.certificateRenewalThreshold != null && init.certificateRenewalThreshold >= init.certificateLifespan) {
|
123
|
+
throw new InvalidParametersError('Certificate renewal threshold must be less than certificate lifespan')
|
124
|
+
}
|
52
125
|
|
53
126
|
if (components.metrics != null) {
|
54
127
|
this.metrics = {
|
@@ -68,43 +141,23 @@ export class WebRTCDirectTransport implements Transport {
|
|
68
141
|
'@libp2p/transport'
|
69
142
|
]
|
70
143
|
|
71
|
-
|
72
|
-
|
73
|
-
*/
|
74
|
-
async dial (ma: Multiaddr, options: DialTransportOptions<WebRTCDialEvents>): Promise<Connection> {
|
75
|
-
const rawConn = await this._connect(ma, options)
|
76
|
-
this.log('dialing address: %a', ma)
|
77
|
-
return rawConn
|
144
|
+
async start (): Promise<void> {
|
145
|
+
this.certificate = await this.getCertificate()
|
78
146
|
}
|
79
147
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
return new WebRTCDirectListener(this.components, {
|
85
|
-
...this.init,
|
86
|
-
...options
|
87
|
-
})
|
88
|
-
}
|
89
|
-
|
90
|
-
/**
|
91
|
-
* Filter check for all Multiaddrs that this transport can listen on
|
92
|
-
*/
|
93
|
-
listenFilter (multiaddrs: Multiaddr[]): Multiaddr[] {
|
94
|
-
return multiaddrs.filter(WebRTCDirect.exactMatch)
|
95
|
-
}
|
148
|
+
async stop (): Promise<void> {
|
149
|
+
if (this.renewCertificateTask != null) {
|
150
|
+
clearTimeout(this.renewCertificateTask)
|
151
|
+
}
|
96
152
|
|
97
|
-
|
98
|
-
* Filter check for all Multiaddrs that this transport can dial
|
99
|
-
*/
|
100
|
-
dialFilter (multiaddrs: Multiaddr[]): Multiaddr[] {
|
101
|
-
return this.listenFilter(multiaddrs)
|
153
|
+
this.certificate = undefined
|
102
154
|
}
|
103
155
|
|
104
156
|
/**
|
105
|
-
*
|
157
|
+
* Dial a given multiaddr
|
106
158
|
*/
|
107
|
-
async
|
159
|
+
async dial (ma: Multiaddr, options: DialTransportOptions<WebRTCDialEvents>): Promise<Connection> {
|
160
|
+
this.log('dial %a', ma)
|
108
161
|
// do not create RTCPeerConnection if the signal has already been aborted
|
109
162
|
options.signal.throwIfAborted()
|
110
163
|
|
@@ -120,7 +173,7 @@ export class WebRTCDirectTransport implements Transport {
|
|
120
173
|
const peerConnection = await createDialerRTCPeerConnection('client', ufrag, typeof this.init.rtcConfiguration === 'function' ? await this.init.rtcConfiguration() : this.init.rtcConfiguration ?? {})
|
121
174
|
|
122
175
|
try {
|
123
|
-
return await
|
176
|
+
return await connect(peerConnection, ufrag, {
|
124
177
|
role: 'client',
|
125
178
|
log: this.log,
|
126
179
|
logger: this.components.logger,
|
@@ -133,10 +186,222 @@ export class WebRTCDirectTransport implements Transport {
|
|
133
186
|
peerId: this.components.peerId,
|
134
187
|
remotePeerId: theirPeerId,
|
135
188
|
privateKey: this.components.privateKey
|
136
|
-
})
|
189
|
+
})
|
137
190
|
} catch (err) {
|
138
191
|
peerConnection.close()
|
139
192
|
throw err
|
140
193
|
}
|
141
194
|
}
|
195
|
+
|
196
|
+
/**
|
197
|
+
* Create a transport listener - this will throw in browsers
|
198
|
+
*/
|
199
|
+
createListener (options: CreateListenerOptions): Listener {
|
200
|
+
if (this.certificate == null) {
|
201
|
+
throw new NotStartedError()
|
202
|
+
}
|
203
|
+
|
204
|
+
return new WebRTCDirectListener(this.components, {
|
205
|
+
...this.init,
|
206
|
+
...options,
|
207
|
+
certificate: this.certificate,
|
208
|
+
emitter: this.emitter
|
209
|
+
})
|
210
|
+
}
|
211
|
+
|
212
|
+
/**
|
213
|
+
* Filter check for all Multiaddrs that this transport can listen on
|
214
|
+
*/
|
215
|
+
listenFilter (multiaddrs: Multiaddr[]): Multiaddr[] {
|
216
|
+
return multiaddrs.filter(WebRTCDirect.exactMatch)
|
217
|
+
}
|
218
|
+
|
219
|
+
/**
|
220
|
+
* Filter check for all Multiaddrs that this transport can dial
|
221
|
+
*/
|
222
|
+
dialFilter (multiaddrs: Multiaddr[]): Multiaddr[] {
|
223
|
+
return this.listenFilter(multiaddrs)
|
224
|
+
}
|
225
|
+
|
226
|
+
private async getCertificate (forceRenew?: boolean): Promise<TransportCertificate> {
|
227
|
+
if (isTransportCertificate(this.init.certificate)) {
|
228
|
+
this.log('using provided TLS certificate')
|
229
|
+
return this.init.certificate
|
230
|
+
}
|
231
|
+
|
232
|
+
const privateKey = await this.loadOrCreatePrivateKey()
|
233
|
+
const { pem, certhash } = await this.loadOrCreateCertificate(privateKey, forceRenew)
|
234
|
+
|
235
|
+
return {
|
236
|
+
privateKey: await formatAsPem(privateKey),
|
237
|
+
pem,
|
238
|
+
certhash
|
239
|
+
}
|
240
|
+
}
|
241
|
+
|
242
|
+
private async loadOrCreatePrivateKey (): Promise<PrivateKey> {
|
243
|
+
if (this.privateKey != null) {
|
244
|
+
return this.privateKey
|
245
|
+
}
|
246
|
+
|
247
|
+
const keychainName = this.init.certificateKeychainName ?? DEFAULT_CERTIFICATE_PRIVATE_KEY_NAME
|
248
|
+
const keychain = this.getKeychain()
|
249
|
+
|
250
|
+
try {
|
251
|
+
if (keychain == null) {
|
252
|
+
this.log('no keychain configured - not checking for stored private key')
|
253
|
+
throw new NotFoundError()
|
254
|
+
}
|
255
|
+
|
256
|
+
this.log.trace('checking for stored private key')
|
257
|
+
this.privateKey = await keychain.exportKey(keychainName)
|
258
|
+
} catch (err: any) {
|
259
|
+
if (err.name !== 'NotFoundError') {
|
260
|
+
throw err
|
261
|
+
}
|
262
|
+
|
263
|
+
this.log.trace('generating private key')
|
264
|
+
this.privateKey = await generateKeyPair('ECDSA', 'P-256')
|
265
|
+
|
266
|
+
if (keychain != null) {
|
267
|
+
this.log.trace('storing private key')
|
268
|
+
await keychain.importKey(keychainName, this.privateKey)
|
269
|
+
} else {
|
270
|
+
this.log('no keychain configured - not storing private key')
|
271
|
+
}
|
272
|
+
}
|
273
|
+
|
274
|
+
return this.privateKey
|
275
|
+
}
|
276
|
+
|
277
|
+
private async loadOrCreateCertificate (privateKey: PrivateKey, forceRenew?: boolean): Promise<{ pem: string, certhash: string }> {
|
278
|
+
if (this.certificate != null && forceRenew !== true) {
|
279
|
+
return this.certificate
|
280
|
+
}
|
281
|
+
|
282
|
+
let cert: X509Certificate
|
283
|
+
const dsKey = new Key(this.init.certificateDatastoreKey ?? DEFAULT_CERTIFICATE_DATASTORE_KEY)
|
284
|
+
const keyPair = await privateKeyToCryptoKeyPair(privateKey)
|
285
|
+
|
286
|
+
try {
|
287
|
+
if (forceRenew === true) {
|
288
|
+
this.log.trace('forcing renewal of TLS certificate')
|
289
|
+
throw new NotFoundError()
|
290
|
+
}
|
291
|
+
|
292
|
+
this.log.trace('checking for stored TLS certificate')
|
293
|
+
cert = await this.loadCertificate(dsKey, keyPair)
|
294
|
+
} catch (err: any) {
|
295
|
+
if (err.name !== 'NotFoundError') {
|
296
|
+
throw err
|
297
|
+
}
|
298
|
+
|
299
|
+
this.log.trace('generating new TLS certificate')
|
300
|
+
cert = await this.createCertificate(dsKey, keyPair)
|
301
|
+
}
|
302
|
+
|
303
|
+
// set timeout to renew certificate
|
304
|
+
let renewTime = (cert.notAfter.getTime() - (this.init.certificateRenewalThreshold ?? DEFAULT_CERTIFICATE_RENEWAL_THRESHOLD)) - Date.now()
|
305
|
+
|
306
|
+
if (renewTime < 0) {
|
307
|
+
renewTime = 100
|
308
|
+
}
|
309
|
+
|
310
|
+
this.log('will renew TLS certificate after %d ms', renewTime)
|
311
|
+
|
312
|
+
this.renewCertificateTask = setTimeout(() => {
|
313
|
+
this.log('renewing TLS certificate')
|
314
|
+
this.getCertificate(true)
|
315
|
+
.then(cert => {
|
316
|
+
this.certificate = cert
|
317
|
+
this.emitter.safeDispatchEvent('certificate:renew', {
|
318
|
+
detail: cert
|
319
|
+
})
|
320
|
+
})
|
321
|
+
.catch(err => {
|
322
|
+
this.log.error('could not renew certificate - %e', err)
|
323
|
+
})
|
324
|
+
}, renewTime)
|
325
|
+
|
326
|
+
return {
|
327
|
+
pem: cert.toString('pem'),
|
328
|
+
certhash: base64url.encode((await sha256.digest(new Uint8Array(cert.rawData))).bytes)
|
329
|
+
}
|
330
|
+
}
|
331
|
+
|
332
|
+
async loadCertificate (dsKey: Key, keyPair: CryptoKeyPair): Promise<X509Certificate> {
|
333
|
+
const buf = await this.components.datastore.get(dsKey)
|
334
|
+
const cert = new X509Certificate(buf)
|
335
|
+
|
336
|
+
// check expiry date
|
337
|
+
const expiryTime = cert.notAfter.getTime() - (this.init.certificateRenewalThreshold ?? DEFAULT_CERTIFICATE_RENEWAL_THRESHOLD)
|
338
|
+
|
339
|
+
if (Date.now() > expiryTime) {
|
340
|
+
this.log('stored TLS certificate has expired')
|
341
|
+
// act as if no certificate was present
|
342
|
+
throw new NotFoundError()
|
343
|
+
}
|
344
|
+
|
345
|
+
this.log('loaded certificate, expires in %d ms', expiryTime)
|
346
|
+
|
347
|
+
// check public keys match
|
348
|
+
const exportedCertKey = await cert.publicKey.export(crypto)
|
349
|
+
const rawCertKey = await crypto.subtle.exportKey('raw', exportedCertKey)
|
350
|
+
const rawKeyPairKey = await crypto.subtle.exportKey('raw', keyPair.publicKey)
|
351
|
+
|
352
|
+
if (!uint8ArrayEquals(
|
353
|
+
new Uint8Array(rawCertKey, 0, rawCertKey.byteLength),
|
354
|
+
new Uint8Array(rawKeyPairKey, 0, rawKeyPairKey.byteLength)
|
355
|
+
)) {
|
356
|
+
this.log('stored TLS certificate public key did not match public key from private key')
|
357
|
+
throw new NotFoundError()
|
358
|
+
}
|
359
|
+
|
360
|
+
this.log('loaded certificate, expiry time is %o', expiryTime)
|
361
|
+
|
362
|
+
return cert
|
363
|
+
}
|
364
|
+
|
365
|
+
async createCertificate (dsKey: Key, keyPair: CryptoKeyPair): Promise<X509Certificate> {
|
366
|
+
const notBefore = new Date()
|
367
|
+
const notAfter = new Date(Date.now() + (this.init.certificateLifespan ?? DEFAULT_CERTIFICATE_LIFESPAN))
|
368
|
+
|
369
|
+
// have to set ms to 0 to work around https://github.com/PeculiarVentures/x509/issues/73
|
370
|
+
notBefore.setMilliseconds(0)
|
371
|
+
notAfter.setMilliseconds(0)
|
372
|
+
|
373
|
+
const cert = await X509CertificateGenerator.createSelfSigned({
|
374
|
+
serialNumber: (BigInt(Math.random().toString().replace('.', '')) * 100000n).toString(16),
|
375
|
+
name: 'CN=example.com, C=US, L=CA, O=example, ST=CA',
|
376
|
+
notBefore,
|
377
|
+
notAfter,
|
378
|
+
keys: keyPair,
|
379
|
+
extensions: [
|
380
|
+
new BasicConstraintsExtension(false, undefined, true)
|
381
|
+
]
|
382
|
+
}, crypto)
|
383
|
+
|
384
|
+
if (this.getKeychain() != null) {
|
385
|
+
this.log.trace('storing TLS certificate')
|
386
|
+
await this.components.datastore.put(dsKey, uint8ArrayFromString(cert.toString('pem')))
|
387
|
+
} else {
|
388
|
+
this.log('no keychain is configured so not storing TLS certificate since the private key will not be reused')
|
389
|
+
}
|
390
|
+
|
391
|
+
return cert
|
392
|
+
}
|
393
|
+
|
394
|
+
private getKeychain (): Keychain | undefined {
|
395
|
+
try {
|
396
|
+
return this.components.keychain
|
397
|
+
} catch {}
|
398
|
+
}
|
399
|
+
}
|
400
|
+
|
401
|
+
function isTransportCertificate (obj?: any): obj is TransportCertificate {
|
402
|
+
if (obj == null) {
|
403
|
+
return false
|
404
|
+
}
|
405
|
+
|
406
|
+
return typeof obj.privateKey === 'string' && typeof obj.pem === 'string' && typeof obj.certhash === 'string'
|
142
407
|
}
|