@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.
Files changed (40) hide show
  1. package/README.md +54 -0
  2. package/dist/index.min.js +153 -18
  3. package/dist/src/constants.d.ts +20 -0
  4. package/dist/src/constants.d.ts.map +1 -1
  5. package/dist/src/constants.js +20 -0
  6. package/dist/src/constants.js.map +1 -1
  7. package/dist/src/index.d.ts +54 -0
  8. package/dist/src/index.d.ts.map +1 -1
  9. package/dist/src/index.js +54 -0
  10. package/dist/src/index.js.map +1 -1
  11. package/dist/src/private-to-private/util.d.ts +2 -2
  12. package/dist/src/private-to-private/util.d.ts.map +1 -1
  13. package/dist/src/private-to-public/listener.d.ts +9 -4
  14. package/dist/src/private-to-public/listener.d.ts.map +1 -1
  15. package/dist/src/private-to-public/listener.js +8 -18
  16. package/dist/src/private-to-public/listener.js.map +1 -1
  17. package/dist/src/private-to-public/transport.d.ts +68 -10
  18. package/dist/src/private-to-public/transport.d.ts.map +1 -1
  19. package/dist/src/private-to-public/transport.js +204 -33
  20. package/dist/src/private-to-public/transport.js.map +1 -1
  21. package/dist/src/private-to-public/utils/connect.js +126 -124
  22. package/dist/src/private-to-public/utils/connect.js.map +1 -1
  23. package/dist/src/private-to-public/utils/pem.d.ts +6 -0
  24. package/dist/src/private-to-public/utils/pem.d.ts.map +1 -0
  25. package/dist/src/private-to-public/utils/pem.js +15 -0
  26. package/dist/src/private-to-public/utils/pem.js.map +1 -0
  27. package/dist/src/stream.d.ts +5 -0
  28. package/dist/src/stream.d.ts.map +1 -1
  29. package/dist/src/stream.js +14 -9
  30. package/dist/src/stream.js.map +1 -1
  31. package/package.json +12 -9
  32. package/src/constants.ts +25 -0
  33. package/src/index.ts +54 -0
  34. package/src/private-to-private/util.ts +2 -2
  35. package/src/private-to-public/listener.ts +18 -26
  36. package/src/private-to-public/transport.ts +304 -39
  37. package/src/private-to-public/utils/connect.ts +139 -135
  38. package/src/private-to-public/utils/pem.ts +18 -0
  39. package/src/stream.ts +20 -11
  40. package/dist/typedoc-urls.json +0 -14
@@ -1,20 +1,27 @@
1
- import { serviceCapabilities, transportSymbol } from '@libp2p/interface'
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 { raceSignal } from 'race-signal'
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 class WebRTCDirectTransport implements Transport {
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
- * Dial a given multiaddr
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
- * Create transport listeners no supported by browsers
82
- */
83
- createListener (options: CreateListenerOptions): Listener {
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
- * Connect to a peer using a multiaddr
157
+ * Dial a given multiaddr
106
158
  */
107
- async _connect (ma: Multiaddr, options: DialTransportOptions<WebRTCDialEvents>): Promise<Connection> {
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 raceSignal(connect(peerConnection, ufrag, {
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
- }), options.signal)
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
  }