@libp2p/webrtc 5.0.26-d8f003e6e → 5.0.27-2e35b6055

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 (119) hide show
  1. package/README.md +16 -21
  2. package/dist/index.min.js +31 -12
  3. package/dist/src/constants.d.ts +2 -0
  4. package/dist/src/constants.d.ts.map +1 -1
  5. package/dist/src/constants.js +2 -0
  6. package/dist/src/constants.js.map +1 -1
  7. package/dist/src/index.d.ts +33 -21
  8. package/dist/src/index.d.ts.map +1 -1
  9. package/dist/src/index.js +16 -21
  10. package/dist/src/index.js.map +1 -1
  11. package/dist/src/maconn.d.ts +1 -0
  12. package/dist/src/maconn.d.ts.map +1 -1
  13. package/dist/src/maconn.js +4 -3
  14. package/dist/src/maconn.js.map +1 -1
  15. package/dist/src/muxer.d.ts +1 -0
  16. package/dist/src/muxer.d.ts.map +1 -1
  17. package/dist/src/muxer.js +2 -2
  18. package/dist/src/muxer.js.map +1 -1
  19. package/dist/src/private-to-private/util.d.ts +1 -0
  20. package/dist/src/private-to-private/util.d.ts.map +1 -1
  21. package/dist/src/private-to-private/util.js.map +1 -1
  22. package/dist/src/private-to-public/listener.browser.d.ts +17 -0
  23. package/dist/src/private-to-public/listener.browser.d.ts.map +1 -0
  24. package/dist/src/private-to-public/listener.browser.js +13 -0
  25. package/dist/src/private-to-public/listener.browser.js.map +1 -0
  26. package/dist/src/private-to-public/listener.d.ts +37 -0
  27. package/dist/src/private-to-public/listener.d.ts.map +1 -0
  28. package/dist/src/private-to-public/listener.js +175 -0
  29. package/dist/src/private-to-public/listener.js.map +1 -0
  30. package/dist/src/private-to-public/pb/message.d.ts.map +1 -0
  31. package/dist/src/private-to-public/pb/message.js.map +1 -0
  32. package/dist/src/private-to-public/transport.d.ts +10 -11
  33. package/dist/src/private-to-public/transport.d.ts.map +1 -1
  34. package/dist/src/private-to-public/transport.js +28 -155
  35. package/dist/src/private-to-public/transport.js.map +1 -1
  36. package/dist/src/private-to-public/utils/connect.d.ts +27 -0
  37. package/dist/src/private-to-public/utils/connect.d.ts.map +1 -0
  38. package/dist/src/private-to-public/utils/connect.js +142 -0
  39. package/dist/src/private-to-public/utils/connect.js.map +1 -0
  40. package/dist/src/private-to-public/utils/generate-certificates.browser.d.ts +2 -0
  41. package/dist/src/private-to-public/utils/generate-certificates.browser.d.ts.map +1 -0
  42. package/dist/src/private-to-public/utils/generate-certificates.browser.js +4 -0
  43. package/dist/src/private-to-public/utils/generate-certificates.browser.js.map +1 -0
  44. package/dist/src/private-to-public/utils/generate-certificates.d.ts +8 -0
  45. package/dist/src/private-to-public/utils/generate-certificates.d.ts.map +1 -0
  46. package/dist/src/private-to-public/utils/generate-certificates.js +39 -0
  47. package/dist/src/private-to-public/utils/generate-certificates.js.map +1 -0
  48. package/dist/src/private-to-public/utils/generate-noise-prologue.d.ts +7 -0
  49. package/dist/src/private-to-public/utils/generate-noise-prologue.d.ts.map +1 -0
  50. package/dist/src/private-to-public/utils/generate-noise-prologue.js +22 -0
  51. package/dist/src/private-to-public/utils/generate-noise-prologue.js.map +1 -0
  52. package/dist/src/private-to-public/utils/get-rtcpeerconnection.browser.d.ts +2 -0
  53. package/dist/src/private-to-public/utils/get-rtcpeerconnection.browser.d.ts.map +1 -0
  54. package/dist/src/private-to-public/utils/get-rtcpeerconnection.browser.js +20 -0
  55. package/dist/src/private-to-public/utils/get-rtcpeerconnection.browser.js.map +1 -0
  56. package/dist/src/private-to-public/utils/get-rtcpeerconnection.d.ts +19 -0
  57. package/dist/src/private-to-public/utils/get-rtcpeerconnection.d.ts.map +1 -0
  58. package/dist/src/private-to-public/utils/get-rtcpeerconnection.js +86 -0
  59. package/dist/src/private-to-public/utils/get-rtcpeerconnection.js.map +1 -0
  60. package/dist/src/private-to-public/utils/sdp.d.ts +36 -0
  61. package/dist/src/private-to-public/utils/sdp.d.ts.map +1 -0
  62. package/dist/src/private-to-public/{sdp.js → utils/sdp.js} +72 -57
  63. package/dist/src/private-to-public/utils/sdp.js.map +1 -0
  64. package/dist/src/private-to-public/utils/stun-listener.d.ts +15 -0
  65. package/dist/src/private-to-public/utils/stun-listener.d.ts.map +1 -0
  66. package/dist/src/private-to-public/utils/stun-listener.js +79 -0
  67. package/dist/src/private-to-public/utils/stun-listener.js.map +1 -0
  68. package/dist/src/stream.d.ts +2 -0
  69. package/dist/src/stream.d.ts.map +1 -1
  70. package/dist/src/stream.js +56 -12
  71. package/dist/src/stream.js.map +1 -1
  72. package/dist/src/util.d.ts +4 -0
  73. package/dist/src/util.d.ts.map +1 -1
  74. package/dist/src/util.js +7 -1
  75. package/dist/src/util.js.map +1 -1
  76. package/dist/src/webrtc/index.d.ts +2 -1
  77. package/dist/src/webrtc/index.d.ts.map +1 -1
  78. package/dist/src/webrtc/index.js +1 -1
  79. package/dist/src/webrtc/index.js.map +1 -1
  80. package/package.json +22 -11
  81. package/src/constants.ts +4 -0
  82. package/src/index.ts +35 -21
  83. package/src/maconn.ts +5 -3
  84. package/src/muxer.ts +3 -2
  85. package/src/private-to-private/util.ts +1 -0
  86. package/src/private-to-public/listener.browser.ts +28 -0
  87. package/src/private-to-public/listener.ts +233 -0
  88. package/src/private-to-public/transport.ts +39 -182
  89. package/src/private-to-public/utils/connect.ts +192 -0
  90. package/src/private-to-public/utils/generate-certificates.browser.ts +3 -0
  91. package/src/private-to-public/utils/generate-certificates.ts +51 -0
  92. package/src/private-to-public/utils/generate-noise-prologue.ts +26 -0
  93. package/src/private-to-public/utils/get-rtcpeerconnection.browser.ts +22 -0
  94. package/src/private-to-public/utils/get-rtcpeerconnection.ts +108 -0
  95. package/src/private-to-public/utils/sdp.ts +174 -0
  96. package/src/private-to-public/utils/stun-listener.ts +104 -0
  97. package/src/stream.ts +68 -15
  98. package/src/util.ts +11 -1
  99. package/src/webrtc/index.ts +2 -1
  100. package/dist/src/pb/message.d.ts.map +0 -1
  101. package/dist/src/pb/message.js.map +0 -1
  102. package/dist/src/private-to-public/options.d.ts +0 -6
  103. package/dist/src/private-to-public/options.d.ts.map +0 -1
  104. package/dist/src/private-to-public/options.js +0 -2
  105. package/dist/src/private-to-public/options.js.map +0 -1
  106. package/dist/src/private-to-public/sdp.d.ts +0 -31
  107. package/dist/src/private-to-public/sdp.d.ts.map +0 -1
  108. package/dist/src/private-to-public/sdp.js.map +0 -1
  109. package/dist/src/private-to-public/util.d.ts +0 -2
  110. package/dist/src/private-to-public/util.d.ts.map +0 -1
  111. package/dist/src/private-to-public/util.js +0 -3
  112. package/dist/src/private-to-public/util.js.map +0 -1
  113. package/src/private-to-public/options.ts +0 -4
  114. package/src/private-to-public/sdp.ts +0 -159
  115. package/src/private-to-public/util.ts +0 -2
  116. /package/dist/src/{pb → private-to-public/pb}/message.d.ts +0 -0
  117. /package/dist/src/{pb → private-to-public/pb}/message.js +0 -0
  118. /package/src/{pb → private-to-public/pb}/message.proto +0 -0
  119. /package/src/{pb → private-to-public/pb}/message.ts +0 -0
@@ -0,0 +1,192 @@
1
+ import { noise } from '@chainsafe/libp2p-noise'
2
+ import { raceEvent } from 'race-event'
3
+ import { WebRTCTransportError } from '../../error.js'
4
+ import { WebRTCMultiaddrConnection } from '../../maconn.js'
5
+ import { DataChannelMuxerFactory } from '../../muxer.js'
6
+ import { createStream } from '../../stream.js'
7
+ import { isFirefox } from '../../util.js'
8
+ import { generateNoisePrologue } from './generate-noise-prologue.js'
9
+ import * as sdp from './sdp.js'
10
+ import type { DirectRTCPeerConnection } from './get-rtcpeerconnection.js'
11
+ import type { DataChannelOptions } from '../../index.js'
12
+ import type { ComponentLogger, Connection, CounterGroup, Logger, Metrics, PeerId, PrivateKey, Upgrader } from '@libp2p/interface'
13
+ import type { Multiaddr } from '@multiformats/multiaddr'
14
+
15
+ export interface ConnectOptions {
16
+ log: Logger
17
+ logger: ComponentLogger
18
+ metrics?: Metrics
19
+ events?: CounterGroup
20
+ remoteAddr: Multiaddr
21
+ role: 'client' | 'server'
22
+ dataChannel?: DataChannelOptions
23
+ upgrader: Upgrader
24
+ peerId: PeerId
25
+ remotePeerId?: PeerId
26
+ signal: AbortSignal
27
+ privateKey: PrivateKey
28
+ }
29
+
30
+ export interface ClientOptions extends ConnectOptions {
31
+ role: 'client'
32
+ }
33
+
34
+ export interface ServerOptions extends ConnectOptions {
35
+ role: 'server'
36
+ }
37
+
38
+ const CONNECTION_STATE_CHANGE_EVENT = isFirefox ? 'iceconnectionstatechange' : 'connectionstatechange'
39
+
40
+ export async function connect (peerConnection: DirectRTCPeerConnection, ufrag: string, options: ClientOptions): Promise<Connection>
41
+ export async function connect (peerConnection: DirectRTCPeerConnection, ufrag: string, options: ServerOptions): Promise<void>
42
+ export async function connect (peerConnection: DirectRTCPeerConnection, ufrag: string, options: ConnectOptions): Promise<any> {
43
+ // create data channel for running the noise handshake. Once the data
44
+ // channel is opened, the remote will initiate the noise handshake. This
45
+ // is used to confirm the identity of the peer.
46
+ const handshakeDataChannel = peerConnection.createDataChannel('', { negotiated: true, id: 0 })
47
+
48
+ if (options.role === 'client') {
49
+ // the client has to set the local offer before the remote answer
50
+
51
+ // Create offer and munge sdp with ufrag == pwd. This allows the remote to
52
+ // respond to STUN messages without performing an actual SDP exchange.
53
+ // This is because it can infer the passwd field by reading the USERNAME
54
+ // attribute of the STUN message.
55
+ options.log.trace('client creating local offer')
56
+ const offerSdp = await peerConnection.createOffer()
57
+ options.log.trace('client created local offer %s', offerSdp.sdp)
58
+ const mungedOfferSdp = sdp.munge(offerSdp, ufrag)
59
+ options.log.trace('client setting local offer %s', mungedOfferSdp.sdp)
60
+ await peerConnection.setLocalDescription(mungedOfferSdp)
61
+
62
+ const answerSdp = sdp.serverAnswerFromMultiaddr(options.remoteAddr, ufrag)
63
+ options.log.trace('client setting server description %s', answerSdp.sdp)
64
+ await peerConnection.setRemoteDescription(answerSdp)
65
+ } else {
66
+ // the server has to set the remote offer before the local answer
67
+ const offerSdp = sdp.clientOfferFromMultiAddr(options.remoteAddr, ufrag)
68
+ options.log.trace('server setting client %s %s', offerSdp.type, offerSdp.sdp)
69
+ await peerConnection.setRemoteDescription(offerSdp)
70
+
71
+ // Create offer and munge sdp with ufrag == pwd. This allows the remote to
72
+ // respond to STUN messages without performing an actual SDP exchange.
73
+ // This is because it can infer the passwd field by reading the USERNAME
74
+ // attribute of the STUN message.
75
+ options.log.trace('server creating local answer')
76
+ const answerSdp = await peerConnection.createAnswer()
77
+ options.log.trace('server created local answer')
78
+ const mungedAnswerSdp = sdp.munge(answerSdp, ufrag)
79
+ options.log.trace('server setting local description %s', answerSdp.sdp)
80
+ await peerConnection.setLocalDescription(mungedAnswerSdp)
81
+ }
82
+
83
+ options.log.trace('%s wait for handshake channel to open', options.role)
84
+ await raceEvent(handshakeDataChannel, 'open', options.signal)
85
+
86
+ options.log.trace('%s handshake channel opened', options.role)
87
+
88
+ if (options.role === 'server') {
89
+ // now that the connection has been opened, add the remote's certhash to
90
+ // it's multiaddr so we can complete the noise handshake
91
+ const remoteFingerprint = peerConnection.remoteFingerprint()?.value ?? ''
92
+ options.remoteAddr = options.remoteAddr.encapsulate(sdp.fingerprint2Ma(remoteFingerprint))
93
+ }
94
+
95
+ // Do noise handshake.
96
+ // Set the Noise Prologue to libp2p-webrtc-noise:<FINGERPRINTS> before
97
+ // starting the actual Noise handshake.
98
+ // <FINGERPRINTS> is the concatenation of the of the two TLS fingerprints
99
+ // of A (responder) and B (initiator) in their byte representation.
100
+ const localFingerprint = sdp.getFingerprintFromSdp(peerConnection.localDescription?.sdp)
101
+
102
+ if (localFingerprint == null) {
103
+ throw new WebRTCTransportError('Could not get fingerprint from local description sdp')
104
+ }
105
+
106
+ options.log.trace('%s performing noise handshake', options.role)
107
+ const noisePrologue = generateNoisePrologue(localFingerprint, options.remoteAddr, options.role)
108
+
109
+ // Since we use the default crypto interface and do not use a static key
110
+ // or early data, we pass in undefined for these parameters.
111
+ const connectionEncrypter = noise({ prologueBytes: noisePrologue })(options)
112
+
113
+ const wrappedChannel = createStream({
114
+ channel: handshakeDataChannel,
115
+ direction: 'inbound',
116
+ logger: options.logger,
117
+ ...(options.dataChannel ?? {})
118
+ })
119
+ const wrappedDuplex = {
120
+ ...wrappedChannel,
121
+ sink: wrappedChannel.sink.bind(wrappedChannel),
122
+ source: (async function * () {
123
+ for await (const list of wrappedChannel.source) {
124
+ for (const buf of list) {
125
+ yield buf
126
+ }
127
+ }
128
+ }())
129
+ }
130
+
131
+ // Creating the connection before completion of the noise
132
+ // handshake ensures that the stream opening callback is set up
133
+ const maConn = new WebRTCMultiaddrConnection(options, {
134
+ peerConnection,
135
+ remoteAddr: options.remoteAddr,
136
+ timeline: {
137
+ open: Date.now()
138
+ },
139
+ metrics: options.events
140
+ })
141
+
142
+ peerConnection.addEventListener(CONNECTION_STATE_CHANGE_EVENT, () => {
143
+ switch (peerConnection.connectionState) {
144
+ case 'failed':
145
+ case 'disconnected':
146
+ case 'closed':
147
+ maConn.close().catch((err) => {
148
+ options.log.error('error closing connection', err)
149
+ })
150
+ break
151
+ default:
152
+ break
153
+ }
154
+ })
155
+
156
+ // Track opened peer connection
157
+ options.events?.increment({ peer_connection: true })
158
+
159
+ const muxerFactory = new DataChannelMuxerFactory(options, {
160
+ peerConnection,
161
+ metrics: options.events,
162
+ dataChannelOptions: options.dataChannel
163
+ })
164
+
165
+ if (options.role === 'client') {
166
+ // For outbound connections, the remote is expected to start the noise handshake.
167
+ // Therefore, we need to secure an inbound noise connection from the remote.
168
+ options.log.trace('%s secure inbound', options.role)
169
+ await connectionEncrypter.secureInbound(wrappedDuplex, {
170
+ remotePeer: options.remotePeerId
171
+ })
172
+
173
+ options.log.trace('%s upgrade outbound', options.role)
174
+ return options.upgrader.upgradeOutbound(maConn, { skipProtection: true, skipEncryption: true, muxerFactory })
175
+ }
176
+
177
+ // For inbound connections, we are expected to start the noise handshake.
178
+ // Therefore, we need to secure an outbound noise connection from the remote.
179
+ options.log.trace('%s secure outbound', options.role)
180
+ const result = await connectionEncrypter.secureOutbound(wrappedDuplex, {
181
+ remotePeer: options.remotePeerId
182
+ })
183
+ maConn.remoteAddr = maConn.remoteAddr.encapsulate(`/p2p/${result.remotePeer}`)
184
+
185
+ options.log.trace('%s upgrade inbound', options.role)
186
+
187
+ await options.upgrader.upgradeInbound(maConn, {
188
+ skipProtection: true,
189
+ skipEncryption: true,
190
+ muxerFactory
191
+ })
192
+ }
@@ -0,0 +1,3 @@
1
+ export async function generateWebTransportCertificate (): Promise<any> {
2
+ throw new Error('Not implemented')
3
+ }
@@ -0,0 +1,51 @@
1
+ import { Crypto } from '@peculiar/webcrypto'
2
+ import * as x509 from '@peculiar/x509'
3
+ import { base64url } from 'multiformats/bases/base64'
4
+ import { sha256 } from 'multiformats/hashes/sha2'
5
+ import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
6
+ import type { TransportCertificate } from '../..'
7
+
8
+ const crypto = new Crypto()
9
+ x509.cryptoProvider.set(crypto)
10
+
11
+ const ONE_DAY_MS = 86400000
12
+
13
+ export interface GenerateTransportCertificateOptions {
14
+ days: number
15
+ start?: Date
16
+ extensions?: any[]
17
+ }
18
+
19
+ export async function generateTransportCertificate (keyPair: CryptoKeyPair, options: GenerateTransportCertificateOptions): Promise<TransportCertificate> {
20
+ const notBefore = options.start ?? new Date()
21
+ notBefore.setMilliseconds(0)
22
+ const notAfter = new Date(notBefore.getTime() + (options.days * ONE_DAY_MS))
23
+ notAfter.setMilliseconds(0)
24
+
25
+ const cert = await x509.X509CertificateGenerator.createSelfSigned({
26
+ serialNumber: (BigInt(Math.random().toString().replace('.', '')) * 100000n).toString(16),
27
+ name: 'CN=ca.com, C=US, L=CA, O=example, ST=CA',
28
+ notBefore,
29
+ notAfter,
30
+ signingAlgorithm: {
31
+ name: 'ECDSA'
32
+ },
33
+ keys: keyPair,
34
+ extensions: [
35
+ new x509.BasicConstraintsExtension(false, undefined, true)
36
+ ]
37
+ })
38
+
39
+ const exported = await crypto.subtle.exportKey('pkcs8', keyPair.privateKey)
40
+ const privateKeyPem = [
41
+ '-----BEGIN PRIVATE KEY-----',
42
+ ...uint8ArrayToString(new Uint8Array(exported), 'base64pad').split(/(.{64})/).filter(Boolean),
43
+ '-----END PRIVATE KEY-----'
44
+ ].join('\n')
45
+
46
+ return {
47
+ privateKey: privateKeyPem,
48
+ pem: cert.toString('pem'),
49
+ certhash: base64url.encode((await sha256.digest(new Uint8Array(cert.rawData))).bytes)
50
+ }
51
+ }
@@ -0,0 +1,26 @@
1
+ import * as Digest from 'multiformats/hashes/digest'
2
+ import { sha256 } from 'multiformats/hashes/sha2'
3
+ import { concat } from 'uint8arrays/concat'
4
+ import { fromString as uint8arrayFromString } from 'uint8arrays/from-string'
5
+ import * as sdp from './sdp.js'
6
+ import type { Multiaddr } from '@multiformats/multiaddr'
7
+
8
+ const PREFIX = uint8arrayFromString('libp2p-webrtc-noise:')
9
+
10
+ /**
11
+ * Generate a noise prologue from the peer connection's certificate.
12
+ * noise prologue = bytes('libp2p-webrtc-noise:') + noise-server fingerprint + noise-client fingerprint
13
+ */
14
+ export function generateNoisePrologue (localFingerprint: string, remoteAddr: Multiaddr, role: 'client' | 'server'): Uint8Array {
15
+ const localFpString = localFingerprint.trim().toLowerCase().replaceAll(':', '')
16
+ const localFpArray = uint8arrayFromString(localFpString, 'hex')
17
+ const local = Digest.create(sha256.code, localFpArray)
18
+ const remote: Uint8Array = sdp.mbdecoder.decode(sdp.certhash(remoteAddr))
19
+ const byteLength = PREFIX.byteLength + local.bytes.byteLength + remote.byteLength
20
+
21
+ if (role === 'server') {
22
+ return concat([PREFIX, remote, local.bytes], byteLength)
23
+ }
24
+
25
+ return concat([PREFIX, local.bytes, remote], byteLength)
26
+ }
@@ -0,0 +1,22 @@
1
+ export async function createDialerRTCPeerConnection (role: 'client' | 'server', ufrag: string, rtcConfiguration?: RTCConfiguration | (() => RTCConfiguration | Promise<RTCConfiguration>), certificate?: RTCCertificate): Promise<RTCPeerConnection> {
2
+ if (certificate == null) {
3
+ // ECDSA is preferred over RSA here. From our testing we find that P-256 elliptic
4
+ // curve is supported by Pion, webrtc-rs, as well as Chromium (P-228 and P-384
5
+ // was not supported in Chromium). We use the same hash function as found in the
6
+ // multiaddr if it is supported.
7
+ certificate = await RTCPeerConnection.generateCertificate({
8
+ name: 'ECDSA',
9
+
10
+ // @ts-expect-error missing from lib.dom.d.ts but required by chrome
11
+ namedCurve: 'P-256'
12
+ // hash: sdp.toSupportedHashFunction(hashName)
13
+ })
14
+ }
15
+
16
+ const rtcConfig = typeof rtcConfiguration === 'function' ? await rtcConfiguration() : rtcConfiguration
17
+
18
+ return new RTCPeerConnection({
19
+ ...(rtcConfig ?? {}),
20
+ certificates: [certificate]
21
+ })
22
+ }
@@ -0,0 +1,108 @@
1
+ import { PeerConnection } from '@ipshipyard/node-datachannel'
2
+ import { RTCPeerConnection } from '@ipshipyard/node-datachannel/polyfill'
3
+ import { Crypto } from '@peculiar/webcrypto'
4
+ import { DEFAULT_ICE_SERVERS } from '../../constants.js'
5
+ import { MAX_MESSAGE_SIZE } from '../../stream.js'
6
+ import { generateTransportCertificate } from './generate-certificates.js'
7
+ import type { TransportCertificate } from '../../index.js'
8
+ import type { CertificateFingerprint } from '@ipshipyard/node-datachannel'
9
+
10
+ const crypto = new Crypto()
11
+
12
+ interface DirectRTCPeerConnectionInit extends RTCConfiguration {
13
+ ufrag: string
14
+ peerConnection: PeerConnection
15
+ }
16
+
17
+ export class DirectRTCPeerConnection extends RTCPeerConnection {
18
+ private readonly peerConnection: PeerConnection
19
+ private readonly ufrag: string
20
+
21
+ constructor (init: DirectRTCPeerConnectionInit) {
22
+ super(init)
23
+
24
+ this.peerConnection = init.peerConnection
25
+ this.ufrag = init.ufrag
26
+ }
27
+
28
+ async createOffer (): Promise<globalThis.RTCSessionDescriptionInit | any> {
29
+ // have to set ufrag before creating offer
30
+ if (this.connectionState === 'new') {
31
+ this.peerConnection?.setLocalDescription('offer', {
32
+ iceUfrag: this.ufrag,
33
+ icePwd: this.ufrag
34
+ })
35
+ }
36
+
37
+ return super.createOffer()
38
+ }
39
+
40
+ async createAnswer (): Promise<globalThis.RTCSessionDescriptionInit | any> {
41
+ // have to set ufrag before creating answer
42
+ if (this.connectionState === 'new') {
43
+ this.peerConnection?.setLocalDescription('answer', {
44
+ iceUfrag: this.ufrag,
45
+ icePwd: this.ufrag
46
+ })
47
+ }
48
+
49
+ return super.createAnswer()
50
+ }
51
+
52
+ remoteFingerprint (): CertificateFingerprint {
53
+ if (this.peerConnection == null) {
54
+ throw new Error('Invalid state: peer connection not set')
55
+ }
56
+
57
+ return this.peerConnection.remoteFingerprint()
58
+ }
59
+ }
60
+
61
+ function mapIceServers (iceServers?: RTCIceServer[]): string[] {
62
+ return iceServers
63
+ ?.map((server) => {
64
+ const urls = Array.isArray(server.urls) ? server.urls : [server.urls]
65
+
66
+ return urls.map((url) => {
67
+ if (server.username != null && server.credential != null) {
68
+ const [protocol, rest] = url.split(/:(.*)/)
69
+ return `${protocol}:${server.username}:${server.credential}@${rest}`
70
+ }
71
+ return url
72
+ })
73
+ })
74
+ .flat() ?? []
75
+ }
76
+
77
+ export async function createDialerRTCPeerConnection (role: 'client' | 'server', ufrag: string, rtcConfiguration?: RTCConfiguration | (() => RTCConfiguration | Promise<RTCConfiguration>), certificate?: TransportCertificate): Promise<DirectRTCPeerConnection> {
78
+ if (certificate == null) {
79
+ // ECDSA is preferred over RSA here. From our testing we find that P-256
80
+ // elliptic curve is supported by Pion, webrtc-rs, as well as Chromium
81
+ // (P-228 and P-384 was not supported in Chromium). We use the same hash
82
+ // function as found in the multiaddr if it is supported.
83
+ const keyPair = await crypto.subtle.generateKey({
84
+ name: 'ECDSA',
85
+ namedCurve: 'P-256'
86
+ }, true, ['sign', 'verify'])
87
+
88
+ certificate = await generateTransportCertificate(keyPair, {
89
+ days: 365
90
+ })
91
+ }
92
+
93
+ const rtcConfig = typeof rtcConfiguration === 'function' ? await rtcConfiguration() : rtcConfiguration
94
+
95
+ return new DirectRTCPeerConnection({
96
+ ...rtcConfig,
97
+ ufrag,
98
+ peerConnection: new PeerConnection(`${role}-${Date.now()}`, {
99
+ disableFingerprintVerification: true,
100
+ disableAutoNegotiation: true,
101
+ certificatePemFile: certificate.pem,
102
+ keyPemFile: certificate.privateKey,
103
+ enableIceUdpMux: role === 'server',
104
+ maxMessageSize: MAX_MESSAGE_SIZE,
105
+ iceServers: mapIceServers(rtcConfig?.iceServers ?? DEFAULT_ICE_SERVERS.map(urls => ({ urls })))
106
+ })
107
+ })
108
+ }
@@ -0,0 +1,174 @@
1
+ import { InvalidParametersError } from '@libp2p/interface'
2
+ import { multiaddr } from '@multiformats/multiaddr'
3
+ import { base64url } from 'multiformats/bases/base64'
4
+ import { bases, digest } from 'multiformats/basics'
5
+ import * as Digest from 'multiformats/hashes/digest'
6
+ import { sha256 } from 'multiformats/hashes/sha2'
7
+ import { InvalidFingerprintError, UnsupportedHashAlgorithmError } from '../../error.js'
8
+ import { MAX_MESSAGE_SIZE } from '../../stream.js'
9
+ import { CERTHASH_CODE } from '../transport.js'
10
+ import type { Multiaddr } from '@multiformats/multiaddr'
11
+ import type { MultihashDigest } from 'multiformats/hashes/interface'
12
+
13
+ /**
14
+ * Get base2 | identity decoders
15
+ */
16
+ // @ts-expect-error - Not easy to combine these types.
17
+ export const mbdecoder: any = Object.values(bases).map(b => b.decoder).reduce((d, b) => d.or(b))
18
+
19
+ const fingerprintRegex = /^a=fingerprint:(?:\w+-[0-9]+)\s(?<fingerprint>(:?[0-9a-fA-F]{2})+)$/m
20
+ export function getFingerprintFromSdp (sdp: string | undefined): string | undefined {
21
+ if (sdp == null) {
22
+ return undefined
23
+ }
24
+
25
+ const searchResult = sdp.match(fingerprintRegex)
26
+ return searchResult?.groups?.fingerprint
27
+ }
28
+
29
+ // Extract the certhash from a multiaddr
30
+ export function certhash (ma: Multiaddr): string {
31
+ const tups = ma.stringTuples()
32
+ const certhash = tups.filter((tup) => tup[0] === CERTHASH_CODE).map((tup) => tup[1])[0]
33
+
34
+ if (certhash === undefined || certhash === '') {
35
+ throw new InvalidParametersError(`Couldn't find a certhash component of multiaddr: ${ma.toString()}`)
36
+ }
37
+
38
+ return certhash
39
+ }
40
+
41
+ /**
42
+ * Convert a certhash into a multihash
43
+ */
44
+ export function decodeCerthash (certhash: string): MultihashDigest {
45
+ return digest.decode(mbdecoder.decode(certhash))
46
+ }
47
+
48
+ export function certhashToFingerprint (certhash: string): string {
49
+ const mbdecoded = decodeCerthash(certhash)
50
+
51
+ return new Array(mbdecoded.bytes.length)
52
+ .fill(0)
53
+ .map((val, index) => {
54
+ return mbdecoded.digest[index].toString(16).padStart(2, '0').toUpperCase()
55
+ })
56
+ .join(':')
57
+ }
58
+
59
+ /**
60
+ * Extract the fingerprint from a multiaddr
61
+ */
62
+ export function ma2Fingerprint (ma: Multiaddr): string {
63
+ const mhdecoded = decodeCerthash(certhash(ma))
64
+ const prefix = toSupportedHashFunction(mhdecoded.code)
65
+ const fingerprint = mhdecoded.digest.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '')
66
+ const sdp = fingerprint.match(/.{1,2}/g)
67
+
68
+ if (sdp == null) {
69
+ throw new InvalidFingerprintError(fingerprint, ma.toString())
70
+ }
71
+
72
+ return `${prefix} ${sdp.join(':').toUpperCase()}`
73
+ }
74
+
75
+ export function fingerprint2Ma (fingerprint: string): Multiaddr {
76
+ const output = fingerprint.split(':').map(str => parseInt(str, 16))
77
+ const encoded = Uint8Array.from(output)
78
+ const digest = Digest.create(sha256.code, encoded)
79
+
80
+ return multiaddr(`/certhash/${base64url.encode(digest.bytes)}`)
81
+ }
82
+
83
+ /**
84
+ * Normalize the hash name from a given multihash has name
85
+ */
86
+ export function toSupportedHashFunction (code: number): 'sha-1' | 'sha-256' | 'sha-512' {
87
+ switch (code) {
88
+ case 0x11:
89
+ return 'sha-1'
90
+ case 0x12:
91
+ return 'sha-256'
92
+ case 0x13:
93
+ return 'sha-512'
94
+ default:
95
+ throw new UnsupportedHashAlgorithmError(code)
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Create an answer SDP message from a multiaddr - the server always operates in
101
+ * ice-lite mode and DTLS active mode.
102
+ */
103
+ export function serverAnswerFromMultiaddr (ma: Multiaddr, ufrag: string): RTCSessionDescriptionInit {
104
+ const { host, port, family } = ma.toOptions()
105
+ const fingerprint = ma2Fingerprint(ma)
106
+ const sdp = `v=0
107
+ o=- 0 0 IN IP${family} ${host}
108
+ s=-
109
+ t=0 0
110
+ a=ice-lite
111
+ m=application ${port} UDP/DTLS/SCTP webrtc-datachannel
112
+ c=IN IP${family} ${host}
113
+ a=mid:0
114
+ a=ice-options:ice2
115
+ a=ice-ufrag:${ufrag}
116
+ a=ice-pwd:${ufrag}
117
+ a=fingerprint:${fingerprint}
118
+ a=setup:passive
119
+ a=sctp-port:5000
120
+ a=max-message-size:${MAX_MESSAGE_SIZE}
121
+ a=candidate:1467250027 1 UDP 1467250027 ${host} ${port} typ host
122
+ a=end-of-candidates
123
+ `
124
+
125
+ return {
126
+ type: 'answer',
127
+ sdp
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Create an offer SDP message from a multiaddr
133
+ */
134
+ export function clientOfferFromMultiAddr (ma: Multiaddr, ufrag: string): RTCSessionDescriptionInit {
135
+ const { host, port, family } = ma.toOptions()
136
+ const sdp = `v=0
137
+ o=- 0 0 IN IP${family} ${host}
138
+ s=-
139
+ c=IN IP${family} ${host}
140
+ t=0 0
141
+ a=ice-options:ice2,trickle
142
+ m=application ${port} UDP/DTLS/SCTP webrtc-datachannel
143
+ a=mid:0
144
+ a=setup:active
145
+ a=ice-ufrag:${ufrag}
146
+ a=ice-pwd:${ufrag}
147
+ a=fingerprint:sha-256 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
148
+ a=sctp-port:5000
149
+ a=max-message-size:${MAX_MESSAGE_SIZE}
150
+ a=candidate:1467250027 1 UDP 1467250027 ${host} ${port} typ host
151
+ a=end-of-candidates
152
+ `
153
+
154
+ return {
155
+ type: 'offer',
156
+ sdp
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Replace (munge) the ufrag and password values in a SDP
162
+ */
163
+ export function munge (desc: RTCSessionDescriptionInit, ufrag: string): RTCSessionDescriptionInit {
164
+ if (desc.sdp === undefined) {
165
+ throw new InvalidParametersError("Can't munge a missing SDP")
166
+ }
167
+
168
+ const lineBreak = desc.sdp.includes('\r\n') ? '\r\n' : '\n'
169
+
170
+ desc.sdp = desc.sdp
171
+ .replace(/\na=ice-ufrag:[^\n]*\n/, '\na=ice-ufrag:' + ufrag + lineBreak)
172
+ .replace(/\na=ice-pwd:[^\n]*\n/, '\na=ice-pwd:' + ufrag + lineBreak)
173
+ return desc
174
+ }
@@ -0,0 +1,104 @@
1
+ import { createSocket } from 'node:dgram'
2
+ import { isIPv4 } from '@chainsafe/is-ip'
3
+ import { IceUdpMuxListener } from '@ipshipyard/node-datachannel'
4
+ import { pEvent } from 'p-event'
5
+ // @ts-expect-error no types
6
+ import stun from 'stun'
7
+ import { UFRAG_PREFIX } from '../../constants.js'
8
+ import type { Logger } from '@libp2p/interface'
9
+ import type { AddressInfo } from 'node:net'
10
+
11
+ export interface StunServer {
12
+ close(): Promise<void>
13
+ address(): AddressInfo
14
+ }
15
+
16
+ export interface Callback {
17
+ (ufrag: string, remoteHost: string, remotePort: number): void
18
+ }
19
+
20
+ async function dgramListener (host: string, port: number, ipVersion: 4 | 6, log: Logger, cb: Callback): Promise<StunServer> {
21
+ const socket = createSocket({
22
+ type: `udp${ipVersion}`,
23
+ reuseAddr: true
24
+ })
25
+
26
+ try {
27
+ socket.bind(port, host)
28
+ await pEvent(socket, 'listening')
29
+ } catch (err) {
30
+ socket.close()
31
+ throw err
32
+ }
33
+
34
+ socket.on('message', (msg, rinfo) => {
35
+ // TODO: this needs to be rate limited keyed by the remote host to
36
+ // prevent a DOS attack
37
+ try {
38
+ log.trace('incoming STUN packet from %o', rinfo)
39
+ const stunMessage = stun.decode(msg)
40
+ const usernameAttribute = stunMessage.getAttribute(stun.constants.STUN_ATTR_USERNAME)
41
+ const username: string | undefined = usernameAttribute?.value?.toString()
42
+
43
+ if (username?.startsWith(UFRAG_PREFIX) !== true) {
44
+ log.trace('ufrag missing from incoming STUN message from %s:%s', rinfo.address, rinfo.port)
45
+ return
46
+ }
47
+
48
+ const [ufrag] = username.split(':')
49
+
50
+ cb(ufrag, rinfo.address, rinfo.port)
51
+ } catch (err) {
52
+ log.error('could not process incoming STUN data from %o', rinfo, err)
53
+ }
54
+ })
55
+
56
+ return {
57
+ close: async () => {
58
+ const p = pEvent(socket, 'close')
59
+ socket.close()
60
+ await p
61
+ },
62
+ address: () => {
63
+ return socket.address()
64
+ }
65
+ }
66
+ }
67
+
68
+ async function libjuiceListener (host: string, port: number, log: Logger, cb: Callback): Promise<StunServer> {
69
+ const listener = new IceUdpMuxListener(port, host)
70
+ listener.onUnhandledStunRequest(request => {
71
+ if (request.ufrag == null) {
72
+ return
73
+ }
74
+
75
+ log.trace('incoming STUN packet from %s:%d %s', request.host, request.port, request.ufrag)
76
+
77
+ cb(request.ufrag, request.host, request.port)
78
+ })
79
+
80
+ return {
81
+ close: async () => {
82
+ listener.stop()
83
+ },
84
+ address: () => {
85
+ return {
86
+ address: host,
87
+ family: isIPv4(host) ? 'IPv4' : 'IPv6',
88
+ port
89
+ }
90
+ }
91
+ }
92
+ }
93
+
94
+ export interface STUNListenerOptions {
95
+ useLibjuice?: boolean
96
+ }
97
+
98
+ export async function stunListener (host: string, port: number, ipVersion: 4 | 6, log: Logger, cb: Callback, opts: STUNListenerOptions = {}): Promise<StunServer> {
99
+ if (opts.useLibjuice === false) {
100
+ return dgramListener(host, port, ipVersion, log, cb)
101
+ }
102
+
103
+ return libjuiceListener(host, port, log, cb)
104
+ }