@libp2p/webrtc 5.0.27 → 5.1.0-1fc0e2662
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 +16 -21
- package/dist/index.min.js +31 -12
- package/dist/src/constants.d.ts +2 -0
- package/dist/src/constants.d.ts.map +1 -1
- package/dist/src/constants.js +2 -0
- package/dist/src/constants.js.map +1 -1
- package/dist/src/index.d.ts +33 -21
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +16 -21
- package/dist/src/index.js.map +1 -1
- package/dist/src/maconn.d.ts +1 -0
- package/dist/src/maconn.d.ts.map +1 -1
- package/dist/src/maconn.js +4 -3
- package/dist/src/maconn.js.map +1 -1
- package/dist/src/muxer.d.ts +1 -0
- package/dist/src/muxer.d.ts.map +1 -1
- package/dist/src/muxer.js +2 -2
- package/dist/src/muxer.js.map +1 -1
- package/dist/src/private-to-private/util.d.ts +1 -0
- package/dist/src/private-to-private/util.d.ts.map +1 -1
- package/dist/src/private-to-private/util.js.map +1 -1
- package/dist/src/private-to-public/listener.browser.d.ts +17 -0
- package/dist/src/private-to-public/listener.browser.d.ts.map +1 -0
- package/dist/src/private-to-public/listener.browser.js +13 -0
- package/dist/src/private-to-public/listener.browser.js.map +1 -0
- package/dist/src/private-to-public/listener.d.ts +37 -0
- package/dist/src/private-to-public/listener.d.ts.map +1 -0
- package/dist/src/private-to-public/listener.js +175 -0
- package/dist/src/private-to-public/listener.js.map +1 -0
- package/dist/src/private-to-public/pb/message.d.ts.map +1 -0
- package/dist/src/private-to-public/pb/message.js.map +1 -0
- package/dist/src/private-to-public/transport.d.ts +10 -11
- package/dist/src/private-to-public/transport.d.ts.map +1 -1
- package/dist/src/private-to-public/transport.js +28 -155
- package/dist/src/private-to-public/transport.js.map +1 -1
- package/dist/src/private-to-public/utils/connect.d.ts +27 -0
- package/dist/src/private-to-public/utils/connect.d.ts.map +1 -0
- package/dist/src/private-to-public/utils/connect.js +142 -0
- package/dist/src/private-to-public/utils/connect.js.map +1 -0
- package/dist/src/private-to-public/utils/generate-certificates.browser.d.ts +2 -0
- package/dist/src/private-to-public/utils/generate-certificates.browser.d.ts.map +1 -0
- package/dist/src/private-to-public/utils/generate-certificates.browser.js +4 -0
- package/dist/src/private-to-public/utils/generate-certificates.browser.js.map +1 -0
- package/dist/src/private-to-public/utils/generate-certificates.d.ts +8 -0
- package/dist/src/private-to-public/utils/generate-certificates.d.ts.map +1 -0
- package/dist/src/private-to-public/utils/generate-certificates.js +39 -0
- package/dist/src/private-to-public/utils/generate-certificates.js.map +1 -0
- package/dist/src/private-to-public/utils/generate-noise-prologue.d.ts +7 -0
- package/dist/src/private-to-public/utils/generate-noise-prologue.d.ts.map +1 -0
- package/dist/src/private-to-public/utils/generate-noise-prologue.js +22 -0
- package/dist/src/private-to-public/utils/generate-noise-prologue.js.map +1 -0
- package/dist/src/private-to-public/utils/get-rtcpeerconnection.browser.d.ts +2 -0
- package/dist/src/private-to-public/utils/get-rtcpeerconnection.browser.d.ts.map +1 -0
- package/dist/src/private-to-public/utils/get-rtcpeerconnection.browser.js +20 -0
- package/dist/src/private-to-public/utils/get-rtcpeerconnection.browser.js.map +1 -0
- package/dist/src/private-to-public/utils/get-rtcpeerconnection.d.ts +19 -0
- package/dist/src/private-to-public/utils/get-rtcpeerconnection.d.ts.map +1 -0
- package/dist/src/private-to-public/utils/get-rtcpeerconnection.js +86 -0
- package/dist/src/private-to-public/utils/get-rtcpeerconnection.js.map +1 -0
- package/dist/src/private-to-public/utils/sdp.d.ts +36 -0
- package/dist/src/private-to-public/utils/sdp.d.ts.map +1 -0
- package/dist/src/private-to-public/{sdp.js → utils/sdp.js} +72 -57
- package/dist/src/private-to-public/utils/sdp.js.map +1 -0
- package/dist/src/private-to-public/utils/stun-listener.d.ts +15 -0
- package/dist/src/private-to-public/utils/stun-listener.d.ts.map +1 -0
- package/dist/src/private-to-public/utils/stun-listener.js +79 -0
- package/dist/src/private-to-public/utils/stun-listener.js.map +1 -0
- package/dist/src/stream.d.ts +2 -0
- package/dist/src/stream.d.ts.map +1 -1
- package/dist/src/stream.js +56 -12
- package/dist/src/stream.js.map +1 -1
- package/dist/src/util.d.ts +4 -0
- package/dist/src/util.d.ts.map +1 -1
- package/dist/src/util.js +7 -1
- package/dist/src/util.js.map +1 -1
- package/dist/src/webrtc/index.d.ts +2 -1
- package/dist/src/webrtc/index.d.ts.map +1 -1
- package/dist/src/webrtc/index.js +1 -1
- package/dist/src/webrtc/index.js.map +1 -1
- package/package.json +23 -12
- package/src/constants.ts +4 -0
- package/src/index.ts +35 -21
- package/src/maconn.ts +5 -3
- package/src/muxer.ts +3 -2
- package/src/private-to-private/util.ts +1 -0
- package/src/private-to-public/listener.browser.ts +28 -0
- package/src/private-to-public/listener.ts +233 -0
- package/src/private-to-public/transport.ts +39 -182
- package/src/private-to-public/utils/connect.ts +192 -0
- package/src/private-to-public/utils/generate-certificates.browser.ts +3 -0
- package/src/private-to-public/utils/generate-certificates.ts +51 -0
- package/src/private-to-public/utils/generate-noise-prologue.ts +26 -0
- package/src/private-to-public/utils/get-rtcpeerconnection.browser.ts +22 -0
- package/src/private-to-public/utils/get-rtcpeerconnection.ts +108 -0
- package/src/private-to-public/utils/sdp.ts +174 -0
- package/src/private-to-public/utils/stun-listener.ts +104 -0
- package/src/stream.ts +68 -15
- package/src/util.ts +11 -1
- package/src/webrtc/index.ts +2 -1
- package/dist/src/pb/message.d.ts.map +0 -1
- package/dist/src/pb/message.js.map +0 -1
- package/dist/src/private-to-public/options.d.ts +0 -6
- package/dist/src/private-to-public/options.d.ts.map +0 -1
- package/dist/src/private-to-public/options.js +0 -2
- package/dist/src/private-to-public/options.js.map +0 -1
- package/dist/src/private-to-public/sdp.d.ts +0 -31
- package/dist/src/private-to-public/sdp.d.ts.map +0 -1
- package/dist/src/private-to-public/sdp.js.map +0 -1
- package/dist/src/private-to-public/util.d.ts +0 -2
- package/dist/src/private-to-public/util.d.ts.map +0 -1
- package/dist/src/private-to-public/util.js +0 -3
- package/dist/src/private-to-public/util.js.map +0 -1
- package/dist/typedoc-urls.json +0 -12
- package/src/private-to-public/options.ts +0 -4
- package/src/private-to-public/sdp.ts +0 -159
- package/src/private-to-public/util.ts +0 -2
- /package/dist/src/{pb → private-to-public/pb}/message.d.ts +0 -0
- /package/dist/src/{pb → private-to-public/pb}/message.js +0 -0
- /package/src/{pb → private-to-public/pb}/message.proto +0 -0
- /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,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
|
+
}
|