Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@libp2p/webrtc

Package Overview
Dependencies
Maintainers
6
Versions
626
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@libp2p/webrtc - npm Package Compare versions

Comparing version 3.0.0 to 3.1.0-c858ca7f

dist/src/webrtc/index.browser.d.ts

1

dist/src/private-to-private/handler.d.ts

@@ -0,1 +1,2 @@

import { RTCPeerConnection } from '../webrtc/index.js';
import type { DataChannelOpts } from '../stream.js';

@@ -2,0 +3,0 @@ import type { Stream } from '@libp2p/interface/connection';

187

dist/src/private-to-private/handler.js

@@ -0,1 +1,2 @@

import { CodeError } from '@libp2p/interface/errors';
import { logger } from '@libp2p/logger';

@@ -6,2 +7,3 @@ import { abortableDuplex } from 'abortable-iterator';

import { DataChannelMuxerFactory } from '../muxer.js';
import { RTCPeerConnection, RTCSessionDescription } from '../webrtc/index.js';
import { Message } from './pb/message.js';

@@ -15,49 +17,58 @@ import { readCandidatesUntilConnected, resolveOnConnected } from './util.js';

const pc = new RTCPeerConnection(rtcConfiguration);
const muxerFactory = new DataChannelMuxerFactory({ peerConnection: pc, dataChannelOptions });
const connectedPromise = pDefer();
const answerSentPromise = pDefer();
signal.onabort = () => { connectedPromise.reject(); };
// candidate callbacks
pc.onicecandidate = ({ candidate }) => {
answerSentPromise.promise.then(async () => {
await stream.write({
type: Message.Type.ICE_CANDIDATE,
data: (candidate != null) ? JSON.stringify(candidate.toJSON()) : ''
try {
const muxerFactory = new DataChannelMuxerFactory({ peerConnection: pc, dataChannelOptions });
const connectedPromise = pDefer();
const answerSentPromise = pDefer();
signal.onabort = () => {
connectedPromise.reject(new CodeError('Timed out while trying to connect', 'ERR_TIMEOUT'));
};
// candidate callbacks
pc.onicecandidate = ({ candidate }) => {
answerSentPromise.promise.then(async () => {
await stream.write({
type: Message.Type.ICE_CANDIDATE,
data: (candidate != null) ? JSON.stringify(candidate.toJSON()) : ''
});
}, (err) => {
log.error('cannot set candidate since sending answer failed', err);
connectedPromise.reject(err);
});
}, (err) => {
log.error('cannot set candidate since sending answer failed', err);
};
resolveOnConnected(pc, connectedPromise);
// read an SDP offer
const pbOffer = await stream.read();
if (pbOffer.type !== Message.Type.SDP_OFFER) {
throw new Error(`expected message type SDP_OFFER, received: ${pbOffer.type ?? 'undefined'} `);
}
const offer = new RTCSessionDescription({
type: 'offer',
sdp: pbOffer.data
});
};
resolveOnConnected(pc, connectedPromise);
// read an SDP offer
const pbOffer = await stream.read();
if (pbOffer.type !== Message.Type.SDP_OFFER) {
throw new Error(`expected message type SDP_OFFER, received: ${pbOffer.type ?? 'undefined'} `);
await pc.setRemoteDescription(offer).catch(err => {
log.error('could not execute setRemoteDescription', err);
throw new Error('Failed to set remoteDescription');
});
// create and write an SDP answer
const answer = await pc.createAnswer().catch(err => {
log.error('could not execute createAnswer', err);
answerSentPromise.reject(err);
throw new Error('Failed to create answer');
});
// write the answer to the remote
await stream.write({ type: Message.Type.SDP_ANSWER, data: answer.sdp });
await pc.setLocalDescription(answer).catch(err => {
log.error('could not execute setLocalDescription', err);
answerSentPromise.reject(err);
throw new Error('Failed to set localDescription');
});
answerSentPromise.resolve();
// wait until candidates are connected
await readCandidatesUntilConnected(connectedPromise, pc, stream);
const remoteAddress = parseRemoteAddress(pc.currentRemoteDescription?.sdp ?? '');
return { pc, muxerFactory, remoteAddress };
}
const offer = new RTCSessionDescription({
type: 'offer',
sdp: pbOffer.data
});
await pc.setRemoteDescription(offer).catch(err => {
log.error('could not execute setRemoteDescription', err);
throw new Error('Failed to set remoteDescription');
});
// create and write an SDP answer
const answer = await pc.createAnswer().catch(err => {
log.error('could not execute createAnswer', err);
answerSentPromise.reject(err);
throw new Error('Failed to create answer');
});
// write the answer to the remote
await stream.write({ type: Message.Type.SDP_ANSWER, data: answer.sdp });
await pc.setLocalDescription(answer).catch(err => {
log.error('could not execute setLocalDescription', err);
answerSentPromise.reject(err);
throw new Error('Failed to set localDescription');
});
answerSentPromise.resolve();
// wait until candidates are connected
await readCandidatesUntilConnected(connectedPromise, pc, stream);
const remoteAddress = parseRemoteAddress(pc.currentRemoteDescription?.sdp ?? '');
return { pc, muxerFactory, remoteAddress };
catch (err) {
pc.close();
throw err;
}
}

@@ -68,45 +79,51 @@ export async function initiateConnection({ rtcConfiguration, dataChannelOptions, signal, stream: rawStream }) {

const pc = new RTCPeerConnection(rtcConfiguration);
const muxerFactory = new DataChannelMuxerFactory({ peerConnection: pc, dataChannelOptions });
const connectedPromise = pDefer();
resolveOnConnected(pc, connectedPromise);
// reject the connectedPromise if the signal aborts
signal.onabort = connectedPromise.reject;
// we create the channel so that the peerconnection has a component for which
// to collect candidates. The label is not relevant to connection initiation
// but can be useful for debugging
const channel = pc.createDataChannel('init');
// setup callback to write ICE candidates to the remote
// peer
pc.onicecandidate = ({ candidate }) => {
void stream.write({
type: Message.Type.ICE_CANDIDATE,
data: (candidate != null) ? JSON.stringify(candidate.toJSON()) : ''
})
.catch(err => {
log.error('error sending ICE candidate', err);
try {
const muxerFactory = new DataChannelMuxerFactory({ peerConnection: pc, dataChannelOptions });
const connectedPromise = pDefer();
resolveOnConnected(pc, connectedPromise);
// reject the connectedPromise if the signal aborts
signal.onabort = connectedPromise.reject;
// we create the channel so that the peerconnection has a component for which
// to collect candidates. The label is not relevant to connection initiation
// but can be useful for debugging
const channel = pc.createDataChannel('init');
// setup callback to write ICE candidates to the remote
// peer
pc.onicecandidate = ({ candidate }) => {
void stream.write({
type: Message.Type.ICE_CANDIDATE,
data: (candidate != null) ? JSON.stringify(candidate.toJSON()) : ''
})
.catch(err => {
log.error('error sending ICE candidate', err);
});
};
// create an offer
const offerSdp = await pc.createOffer();
// write the offer to the stream
await stream.write({ type: Message.Type.SDP_OFFER, data: offerSdp.sdp });
// set offer as local description
await pc.setLocalDescription(offerSdp).catch(err => {
log.error('could not execute setLocalDescription', err);
throw new Error('Failed to set localDescription');
});
};
// create an offer
const offerSdp = await pc.createOffer();
// write the offer to the stream
await stream.write({ type: Message.Type.SDP_OFFER, data: offerSdp.sdp });
// set offer as local description
await pc.setLocalDescription(offerSdp).catch(err => {
log.error('could not execute setLocalDescription', err);
throw new Error('Failed to set localDescription');
});
// read answer
const answerMessage = await stream.read();
if (answerMessage.type !== Message.Type.SDP_ANSWER) {
throw new Error('remote should send an SDP answer');
// read answer
const answerMessage = await stream.read();
if (answerMessage.type !== Message.Type.SDP_ANSWER) {
throw new Error('remote should send an SDP answer');
}
const answerSdp = new RTCSessionDescription({ type: 'answer', sdp: answerMessage.data });
await pc.setRemoteDescription(answerSdp).catch(err => {
log.error('could not execute setRemoteDescription', err);
throw new Error('Failed to set remoteDescription');
});
await readCandidatesUntilConnected(connectedPromise, pc, stream);
channel.close();
const remoteAddress = parseRemoteAddress(pc.currentRemoteDescription?.sdp ?? '');
return { pc, muxerFactory, remoteAddress };
}
const answerSdp = new RTCSessionDescription({ type: 'answer', sdp: answerMessage.data });
await pc.setRemoteDescription(answerSdp).catch(err => {
log.error('could not execute setRemoteDescription', err);
throw new Error('Failed to set remoteDescription');
});
await readCandidatesUntilConnected(connectedPromise, pc, stream);
channel.close();
const remoteAddress = parseRemoteAddress(pc.currentRemoteDescription?.sdp ?? '');
return { pc, muxerFactory, remoteAddress };
catch (err) {
pc.close();
throw err;
}
}

@@ -113,0 +130,0 @@ function parseRemoteAddress(sdp) {

@@ -8,2 +8,3 @@ import { CodeError } from '@libp2p/interface/errors';

import { WebRTCMultiaddrConnection } from '../maconn.js';
import { cleanup } from '../webrtc/index.js';
import { initiateConnection, handleIncomingStream } from './handler.js';

@@ -37,2 +38,3 @@ import { WebRTCPeerListener } from './listener.js';

await this.components.registrar.unhandle(SIGNALING_PROTO_ID);
cleanup();
this._started = false;

@@ -39,0 +41,0 @@ }

import { logger } from '@libp2p/logger';
import { isFirefox } from '../util.js';
import { RTCIceCandidate } from '../webrtc/index.js';
import { Message } from './pb/message.js';

@@ -4,0 +5,0 @@ const log = logger('libp2p:webrtc:peer:util');

@@ -14,2 +14,3 @@ import { noise as Noise } from '@chainsafe/libp2p-noise';

import { isFirefox } from '../util.js';
import { RTCPeerConnection } from '../webrtc/index.js';
import * as sdp from './sdp.js';

@@ -100,95 +101,101 @@ import { genUfrag } from './util.js';

const peerConnection = new RTCPeerConnection({ certificates: [certificate] });
// create data channel for running the noise handshake. Once the data channel is opened,
// the remote will initiate the noise handshake. This is used to confirm the identity of
// the peer.
const dataChannelOpenPromise = new Promise((resolve, reject) => {
const handshakeDataChannel = peerConnection.createDataChannel('', { negotiated: true, id: 0 });
const handshakeTimeout = setTimeout(() => {
const error = `Data channel was never opened: state: ${handshakeDataChannel.readyState}`;
log.error(error);
this.metrics?.dialerEvents.increment({ open_error: true });
reject(dataChannelError('data', error));
}, HANDSHAKE_TIMEOUT_MS);
handshakeDataChannel.onopen = (_) => {
clearTimeout(handshakeTimeout);
resolve(handshakeDataChannel);
try {
// create data channel for running the noise handshake. Once the data channel is opened,
// the remote will initiate the noise handshake. This is used to confirm the identity of
// the peer.
const dataChannelOpenPromise = new Promise((resolve, reject) => {
const handshakeDataChannel = peerConnection.createDataChannel('', { negotiated: true, id: 0 });
const handshakeTimeout = setTimeout(() => {
const error = `Data channel was never opened: state: ${handshakeDataChannel.readyState}`;
log.error(error);
this.metrics?.dialerEvents.increment({ open_error: true });
reject(dataChannelError('data', error));
}, HANDSHAKE_TIMEOUT_MS);
handshakeDataChannel.onopen = (_) => {
clearTimeout(handshakeTimeout);
resolve(handshakeDataChannel);
};
// ref: https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/error_event
handshakeDataChannel.onerror = (event) => {
clearTimeout(handshakeTimeout);
const errorTarget = event.target?.toString() ?? 'not specified';
const error = `Error opening a data channel for handshaking: ${errorTarget}`;
log.error(error);
// NOTE: We use unknown error here but this could potentially be considered a reset by some standards.
this.metrics?.dialerEvents.increment({ unknown_error: true });
reject(dataChannelError('data', error));
};
});
const ufrag = 'libp2p+webrtc+v1/' + genUfrag(32);
// Create offer and munge sdp with ufrag == pwd. This allows the remote to
// respond to STUN messages without performing an actual SDP exchange.
// This is because it can infer the passwd field by reading the USERNAME
// attribute of the STUN message.
const offerSdp = await peerConnection.createOffer();
const mungedOfferSdp = sdp.munge(offerSdp, ufrag);
await peerConnection.setLocalDescription(mungedOfferSdp);
// construct answer sdp from multiaddr and ufrag
const answerSdp = sdp.fromMultiAddr(ma, ufrag);
await peerConnection.setRemoteDescription(answerSdp);
// wait for peerconnection.onopen to fire, or for the datachannel to open
const handshakeDataChannel = await dataChannelOpenPromise;
const myPeerId = this.components.peerId;
// Do noise handshake.
// Set the Noise Prologue to libp2p-webrtc-noise:<FINGERPRINTS> before starting the actual Noise handshake.
// <FINGERPRINTS> is the concatenation of the of the two TLS fingerprints of A and B in their multihash byte representation, sorted in ascending order.
const fingerprintsPrologue = this.generateNoisePrologue(peerConnection, remoteCerthash.code, ma);
// Since we use the default crypto interface and do not use a static key or early data,
// we pass in undefined for these parameters.
const noise = Noise({ prologueBytes: fingerprintsPrologue })();
const wrappedChannel = createStream({ channel: handshakeDataChannel, direction: 'inbound', dataChannelOptions: this.init.dataChannel });
const wrappedDuplex = {
...wrappedChannel,
sink: wrappedChannel.sink.bind(wrappedChannel),
source: (async function* () {
for await (const list of wrappedChannel.source) {
for (const buf of list) {
yield buf;
}
}
}())
};
// ref: https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/error_event
handshakeDataChannel.onerror = (event) => {
clearTimeout(handshakeTimeout);
const errorTarget = event.target?.toString() ?? 'not specified';
const error = `Error opening a data channel for handshaking: ${errorTarget}`;
log.error(error);
// NOTE: We use unknown error here but this could potentially be considered a reset by some standards.
this.metrics?.dialerEvents.increment({ unknown_error: true });
reject(dataChannelError('data', error));
};
});
const ufrag = 'libp2p+webrtc+v1/' + genUfrag(32);
// Create offer and munge sdp with ufrag == pwd. This allows the remote to
// respond to STUN messages without performing an actual SDP exchange.
// This is because it can infer the passwd field by reading the USERNAME
// attribute of the STUN message.
const offerSdp = await peerConnection.createOffer();
const mungedOfferSdp = sdp.munge(offerSdp, ufrag);
await peerConnection.setLocalDescription(mungedOfferSdp);
// construct answer sdp from multiaddr and ufrag
const answerSdp = sdp.fromMultiAddr(ma, ufrag);
await peerConnection.setRemoteDescription(answerSdp);
// wait for peerconnection.onopen to fire, or for the datachannel to open
const handshakeDataChannel = await dataChannelOpenPromise;
const myPeerId = this.components.peerId;
// Do noise handshake.
// Set the Noise Prologue to libp2p-webrtc-noise:<FINGERPRINTS> before starting the actual Noise handshake.
// <FINGERPRINTS> is the concatenation of the of the two TLS fingerprints of A and B in their multihash byte representation, sorted in ascending order.
const fingerprintsPrologue = this.generateNoisePrologue(peerConnection, remoteCerthash.code, ma);
// Since we use the default crypto interface and do not use a static key or early data,
// we pass in undefined for these parameters.
const noise = Noise({ prologueBytes: fingerprintsPrologue })();
const wrappedChannel = createStream({ channel: handshakeDataChannel, direction: 'inbound', dataChannelOptions: this.init.dataChannel });
const wrappedDuplex = {
...wrappedChannel,
sink: wrappedChannel.sink.bind(wrappedChannel),
source: (async function* () {
for await (const list of wrappedChannel.source) {
for (const buf of list) {
yield buf;
}
// Creating the connection before completion of the noise
// handshake ensures that the stream opening callback is set up
const maConn = new WebRTCMultiaddrConnection({
peerConnection,
remoteAddr: ma,
timeline: {
open: Date.now()
},
metrics: this.metrics?.dialerEvents
});
const eventListeningName = isFirefox ? 'iceconnectionstatechange' : 'connectionstatechange';
peerConnection.addEventListener(eventListeningName, () => {
switch (peerConnection.connectionState) {
case 'failed':
case 'disconnected':
case 'closed':
maConn.close().catch((err) => {
log.error('error closing connection', err);
}).finally(() => {
// Remove the event listener once the connection is closed
controller.abort();
});
break;
default:
break;
}
}())
};
// Creating the connection before completion of the noise
// handshake ensures that the stream opening callback is set up
const maConn = new WebRTCMultiaddrConnection({
peerConnection,
remoteAddr: ma,
timeline: {
open: Date.now()
},
metrics: this.metrics?.dialerEvents
});
const eventListeningName = isFirefox ? 'iceconnectionstatechange' : 'connectionstatechange';
peerConnection.addEventListener(eventListeningName, () => {
switch (peerConnection.connectionState) {
case 'failed':
case 'disconnected':
case 'closed':
maConn.close().catch((err) => {
log.error('error closing connection', err);
}).finally(() => {
// Remove the event listener once the connection is closed
controller.abort();
});
break;
default:
break;
}
}, { signal });
// Track opened peer connection
this.metrics?.dialerEvents.increment({ peer_connection: true });
const muxerFactory = new DataChannelMuxerFactory({ peerConnection, metrics: this.metrics?.dialerEvents, dataChannelOptions: this.init.dataChannel });
// For outbound connections, the remote is expected to start the noise handshake.
// Therefore, we need to secure an inbound noise connection from the remote.
await noise.secureInbound(myPeerId, wrappedDuplex, theirPeerId);
return options.upgrader.upgradeOutbound(maConn, { skipProtection: true, skipEncryption: true, muxerFactory });
}, { signal });
// Track opened peer connection
this.metrics?.dialerEvents.increment({ peer_connection: true });
const muxerFactory = new DataChannelMuxerFactory({ peerConnection, metrics: this.metrics?.dialerEvents, dataChannelOptions: this.init.dataChannel });
// For outbound connections, the remote is expected to start the noise handshake.
// Therefore, we need to secure an inbound noise connection from the remote.
await noise.secureInbound(myPeerId, wrappedDuplex, theirPeerId);
return await options.upgrader.upgradeOutbound(maConn, { skipProtection: true, skipEncryption: true, muxerFactory });
}
catch (err) {
peerConnection.close();
throw err;
}
}

@@ -195,0 +202,0 @@ /**

{
"name": "@libp2p/webrtc",
"version": "3.0.0",
"version": "3.1.0-c858ca7f",
"description": "A libp2p transport using WebRTC connections",

@@ -38,3 +38,4 @@ "author": "",

"build": "aegir build",
"test": "aegir test -t browser",
"test": "aegir test -t node -t browser -t electron-main",
"test:node": "aegir test -t node --cov",
"test:chrome": "aegir test -t browser --cov",

@@ -49,6 +50,6 @@ "test:firefox": "aegir test -t browser -- --browser firefox",

"@chainsafe/libp2p-noise": "^12.0.0",
"@libp2p/interface": "^0.1.0",
"@libp2p/interface-internal": "^0.1.0",
"@libp2p/logger": "^3.0.0",
"@libp2p/peer-id": "^3.0.0",
"@libp2p/interface": "0.1.0-c858ca7f",
"@libp2p/interface-internal": "0.1.0-c858ca7f",
"@libp2p/logger": "3.0.0-c858ca7f",
"@libp2p/peer-id": "3.0.0-c858ca7f",
"@multiformats/mafmt": "^12.1.2",

@@ -66,2 +67,3 @@ "@multiformats/multiaddr": "^12.1.3",

"multihashes": "^4.0.3",
"node-datachannel": "^0.4.3",
"p-defer": "^4.0.0",

@@ -75,5 +77,5 @@ "p-event": "^6.0.0",

"@chainsafe/libp2p-yamux": "^4.0.0",
"@libp2p/interface-compliance-tests": "^4.0.0",
"@libp2p/peer-id-factory": "^3.0.0",
"@libp2p/websockets": "^7.0.0",
"@libp2p/interface-compliance-tests": "4.0.0-c858ca7f",
"@libp2p/peer-id-factory": "3.0.0-c858ca7f",
"@libp2p/websockets": "7.0.0-c858ca7f",
"@types/sinon": "^10.0.15",

@@ -85,7 +87,10 @@ "aegir": "^40.0.1",

"it-pair": "^2.0.6",
"libp2p": "^0.46.0",
"libp2p": "0.46.0-c858ca7f",
"protons": "^7.0.2",
"sinon": "^15.1.2",
"sinon-ts": "^1.0.0"
},
"browser": {
"./dist/src/webrtc/index.js": "./dist/src/webrtc/index.browser.js"
}
}

@@ -0,1 +1,2 @@

import { CodeError } from '@libp2p/interface/errors'
import { logger } from '@libp2p/logger'

@@ -6,2 +7,3 @@ import { abortableDuplex } from 'abortable-iterator'

import { DataChannelMuxerFactory } from '../muxer.js'
import { RTCPeerConnection, RTCSessionDescription } from '../webrtc/index.js'
import { Message } from './pb/message.js'

@@ -24,62 +26,71 @@ import { readCandidatesUntilConnected, resolveOnConnected } from './util.js'

const pc = new RTCPeerConnection(rtcConfiguration)
const muxerFactory = new DataChannelMuxerFactory({ peerConnection: pc, dataChannelOptions })
const connectedPromise: DeferredPromise<void> = pDefer()
const answerSentPromise: DeferredPromise<void> = pDefer()
signal.onabort = () => { connectedPromise.reject() }
// candidate callbacks
pc.onicecandidate = ({ candidate }) => {
answerSentPromise.promise.then(
async () => {
await stream.write({
type: Message.Type.ICE_CANDIDATE,
data: (candidate != null) ? JSON.stringify(candidate.toJSON()) : ''
})
},
(err) => {
log.error('cannot set candidate since sending answer failed', err)
}
)
}
try {
const muxerFactory = new DataChannelMuxerFactory({ peerConnection: pc, dataChannelOptions })
const connectedPromise: DeferredPromise<void> = pDefer()
const answerSentPromise: DeferredPromise<void> = pDefer()
resolveOnConnected(pc, connectedPromise)
signal.onabort = () => {
connectedPromise.reject(new CodeError('Timed out while trying to connect', 'ERR_TIMEOUT'))
}
// candidate callbacks
pc.onicecandidate = ({ candidate }) => {
answerSentPromise.promise.then(
async () => {
await stream.write({
type: Message.Type.ICE_CANDIDATE,
data: (candidate != null) ? JSON.stringify(candidate.toJSON()) : ''
})
},
(err) => {
log.error('cannot set candidate since sending answer failed', err)
connectedPromise.reject(err)
}
)
}
// read an SDP offer
const pbOffer = await stream.read()
if (pbOffer.type !== Message.Type.SDP_OFFER) {
throw new Error(`expected message type SDP_OFFER, received: ${pbOffer.type ?? 'undefined'} `)
}
const offer = new RTCSessionDescription({
type: 'offer',
sdp: pbOffer.data
})
resolveOnConnected(pc, connectedPromise)
await pc.setRemoteDescription(offer).catch(err => {
log.error('could not execute setRemoteDescription', err)
throw new Error('Failed to set remoteDescription')
})
// read an SDP offer
const pbOffer = await stream.read()
if (pbOffer.type !== Message.Type.SDP_OFFER) {
throw new Error(`expected message type SDP_OFFER, received: ${pbOffer.type ?? 'undefined'} `)
}
const offer = new RTCSessionDescription({
type: 'offer',
sdp: pbOffer.data
})
// create and write an SDP answer
const answer = await pc.createAnswer().catch(err => {
log.error('could not execute createAnswer', err)
answerSentPromise.reject(err)
throw new Error('Failed to create answer')
})
// write the answer to the remote
await stream.write({ type: Message.Type.SDP_ANSWER, data: answer.sdp })
await pc.setRemoteDescription(offer).catch(err => {
log.error('could not execute setRemoteDescription', err)
throw new Error('Failed to set remoteDescription')
})
await pc.setLocalDescription(answer).catch(err => {
log.error('could not execute setLocalDescription', err)
answerSentPromise.reject(err)
throw new Error('Failed to set localDescription')
})
// create and write an SDP answer
const answer = await pc.createAnswer().catch(err => {
log.error('could not execute createAnswer', err)
answerSentPromise.reject(err)
throw new Error('Failed to create answer')
})
// write the answer to the remote
await stream.write({ type: Message.Type.SDP_ANSWER, data: answer.sdp })
answerSentPromise.resolve()
await pc.setLocalDescription(answer).catch(err => {
log.error('could not execute setLocalDescription', err)
answerSentPromise.reject(err)
throw new Error('Failed to set localDescription')
})
// wait until candidates are connected
await readCandidatesUntilConnected(connectedPromise, pc, stream)
answerSentPromise.resolve()
const remoteAddress = parseRemoteAddress(pc.currentRemoteDescription?.sdp ?? '')
// wait until candidates are connected
await readCandidatesUntilConnected(connectedPromise, pc, stream)
return { pc, muxerFactory, remoteAddress }
const remoteAddress = parseRemoteAddress(pc.currentRemoteDescription?.sdp ?? '')
return { pc, muxerFactory, remoteAddress }
} catch (err) {
pc.close()
throw err
}
}

@@ -98,52 +109,59 @@

const pc = new RTCPeerConnection(rtcConfiguration)
const muxerFactory = new DataChannelMuxerFactory({ peerConnection: pc, dataChannelOptions })
const connectedPromise: DeferredPromise<void> = pDefer()
resolveOnConnected(pc, connectedPromise)
try {
const muxerFactory = new DataChannelMuxerFactory({ peerConnection: pc, dataChannelOptions })
// reject the connectedPromise if the signal aborts
signal.onabort = connectedPromise.reject
// we create the channel so that the peerconnection has a component for which
// to collect candidates. The label is not relevant to connection initiation
// but can be useful for debugging
const channel = pc.createDataChannel('init')
// setup callback to write ICE candidates to the remote
// peer
pc.onicecandidate = ({ candidate }) => {
void stream.write({
type: Message.Type.ICE_CANDIDATE,
data: (candidate != null) ? JSON.stringify(candidate.toJSON()) : ''
})
.catch(err => {
log.error('error sending ICE candidate', err)
const connectedPromise: DeferredPromise<void> = pDefer()
resolveOnConnected(pc, connectedPromise)
// reject the connectedPromise if the signal aborts
signal.onabort = connectedPromise.reject
// we create the channel so that the peerconnection has a component for which
// to collect candidates. The label is not relevant to connection initiation
// but can be useful for debugging
const channel = pc.createDataChannel('init')
// setup callback to write ICE candidates to the remote
// peer
pc.onicecandidate = ({ candidate }) => {
void stream.write({
type: Message.Type.ICE_CANDIDATE,
data: (candidate != null) ? JSON.stringify(candidate.toJSON()) : ''
})
}
// create an offer
const offerSdp = await pc.createOffer()
// write the offer to the stream
await stream.write({ type: Message.Type.SDP_OFFER, data: offerSdp.sdp })
// set offer as local description
await pc.setLocalDescription(offerSdp).catch(err => {
log.error('could not execute setLocalDescription', err)
throw new Error('Failed to set localDescription')
})
.catch(err => {
log.error('error sending ICE candidate', err)
})
}
// read answer
const answerMessage = await stream.read()
if (answerMessage.type !== Message.Type.SDP_ANSWER) {
throw new Error('remote should send an SDP answer')
}
// create an offer
const offerSdp = await pc.createOffer()
// write the offer to the stream
await stream.write({ type: Message.Type.SDP_OFFER, data: offerSdp.sdp })
// set offer as local description
await pc.setLocalDescription(offerSdp).catch(err => {
log.error('could not execute setLocalDescription', err)
throw new Error('Failed to set localDescription')
})
const answerSdp = new RTCSessionDescription({ type: 'answer', sdp: answerMessage.data })
await pc.setRemoteDescription(answerSdp).catch(err => {
log.error('could not execute setRemoteDescription', err)
throw new Error('Failed to set remoteDescription')
})
// read answer
const answerMessage = await stream.read()
if (answerMessage.type !== Message.Type.SDP_ANSWER) {
throw new Error('remote should send an SDP answer')
}
await readCandidatesUntilConnected(connectedPromise, pc, stream)
channel.close()
const answerSdp = new RTCSessionDescription({ type: 'answer', sdp: answerMessage.data })
await pc.setRemoteDescription(answerSdp).catch(err => {
log.error('could not execute setRemoteDescription', err)
throw new Error('Failed to set remoteDescription')
})
const remoteAddress = parseRemoteAddress(pc.currentRemoteDescription?.sdp ?? '')
await readCandidatesUntilConnected(connectedPromise, pc, stream)
channel.close()
return { pc, muxerFactory, remoteAddress }
const remoteAddress = parseRemoteAddress(pc.currentRemoteDescription?.sdp ?? '')
return { pc, muxerFactory, remoteAddress }
} catch (err) {
pc.close()
throw err
}
}

@@ -150,0 +168,0 @@

@@ -8,2 +8,3 @@ import { CodeError } from '@libp2p/interface/errors'

import { WebRTCMultiaddrConnection } from '../maconn.js'
import { cleanup } from '../webrtc/index.js'
import { initiateConnection, handleIncomingStream } from './handler.js'

@@ -61,2 +62,3 @@ import { WebRTCPeerListener } from './listener.js'

await this.components.registrar.unhandle(SIGNALING_PROTO_ID)
cleanup()
this._started = false

@@ -63,0 +65,0 @@ }

import { logger } from '@libp2p/logger'
import { isFirefox } from '../util.js'
import { RTCIceCandidate } from '../webrtc/index.js'
import { Message } from './pb/message.js'

@@ -4,0 +5,0 @@ import type { DeferredPromise } from 'p-defer'

@@ -14,2 +14,3 @@ import { noise as Noise } from '@chainsafe/libp2p-noise'

import { isFirefox } from '../util.js'
import { RTCPeerConnection } from '../webrtc/index.js'
import * as sdp from './sdp.js'

@@ -138,112 +139,117 @@ import { genUfrag } from './util.js'

// create data channel for running the noise handshake. Once the data channel is opened,
// the remote will initiate the noise handshake. This is used to confirm the identity of
// the peer.
const dataChannelOpenPromise = new Promise<RTCDataChannel>((resolve, reject) => {
const handshakeDataChannel = peerConnection.createDataChannel('', { negotiated: true, id: 0 })
const handshakeTimeout = setTimeout(() => {
const error = `Data channel was never opened: state: ${handshakeDataChannel.readyState}`
log.error(error)
this.metrics?.dialerEvents.increment({ open_error: true })
reject(dataChannelError('data', error))
}, HANDSHAKE_TIMEOUT_MS)
try {
// create data channel for running the noise handshake. Once the data channel is opened,
// the remote will initiate the noise handshake. This is used to confirm the identity of
// the peer.
const dataChannelOpenPromise = new Promise<RTCDataChannel>((resolve, reject) => {
const handshakeDataChannel = peerConnection.createDataChannel('', { negotiated: true, id: 0 })
const handshakeTimeout = setTimeout(() => {
const error = `Data channel was never opened: state: ${handshakeDataChannel.readyState}`
log.error(error)
this.metrics?.dialerEvents.increment({ open_error: true })
reject(dataChannelError('data', error))
}, HANDSHAKE_TIMEOUT_MS)
handshakeDataChannel.onopen = (_) => {
clearTimeout(handshakeTimeout)
resolve(handshakeDataChannel)
}
handshakeDataChannel.onopen = (_) => {
clearTimeout(handshakeTimeout)
resolve(handshakeDataChannel)
}
// ref: https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/error_event
handshakeDataChannel.onerror = (event: Event) => {
clearTimeout(handshakeTimeout)
const errorTarget = event.target?.toString() ?? 'not specified'
const error = `Error opening a data channel for handshaking: ${errorTarget}`
log.error(error)
// NOTE: We use unknown error here but this could potentially be considered a reset by some standards.
this.metrics?.dialerEvents.increment({ unknown_error: true })
reject(dataChannelError('data', error))
}
})
// ref: https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/error_event
handshakeDataChannel.onerror = (event: Event) => {
clearTimeout(handshakeTimeout)
const errorTarget = event.target?.toString() ?? 'not specified'
const error = `Error opening a data channel for handshaking: ${errorTarget}`
log.error(error)
// NOTE: We use unknown error here but this could potentially be considered a reset by some standards.
this.metrics?.dialerEvents.increment({ unknown_error: true })
reject(dataChannelError('data', error))
}
})
const ufrag = 'libp2p+webrtc+v1/' + genUfrag(32)
const ufrag = 'libp2p+webrtc+v1/' + genUfrag(32)
// Create offer and munge sdp with ufrag == pwd. This allows the remote to
// respond to STUN messages without performing an actual SDP exchange.
// This is because it can infer the passwd field by reading the USERNAME
// attribute of the STUN message.
const offerSdp = await peerConnection.createOffer()
const mungedOfferSdp = sdp.munge(offerSdp, ufrag)
await peerConnection.setLocalDescription(mungedOfferSdp)
// Create offer and munge sdp with ufrag == pwd. This allows the remote to
// respond to STUN messages without performing an actual SDP exchange.
// This is because it can infer the passwd field by reading the USERNAME
// attribute of the STUN message.
const offerSdp = await peerConnection.createOffer()
const mungedOfferSdp = sdp.munge(offerSdp, ufrag)
await peerConnection.setLocalDescription(mungedOfferSdp)
// construct answer sdp from multiaddr and ufrag
const answerSdp = sdp.fromMultiAddr(ma, ufrag)
await peerConnection.setRemoteDescription(answerSdp)
// construct answer sdp from multiaddr and ufrag
const answerSdp = sdp.fromMultiAddr(ma, ufrag)
await peerConnection.setRemoteDescription(answerSdp)
// wait for peerconnection.onopen to fire, or for the datachannel to open
const handshakeDataChannel = await dataChannelOpenPromise
// wait for peerconnection.onopen to fire, or for the datachannel to open
const handshakeDataChannel = await dataChannelOpenPromise
const myPeerId = this.components.peerId
const myPeerId = this.components.peerId
// Do noise handshake.
// Set the Noise Prologue to libp2p-webrtc-noise:<FINGERPRINTS> before starting the actual Noise handshake.
// <FINGERPRINTS> is the concatenation of the of the two TLS fingerprints of A and B in their multihash byte representation, sorted in ascending order.
const fingerprintsPrologue = this.generateNoisePrologue(peerConnection, remoteCerthash.code, ma)
// Do noise handshake.
// Set the Noise Prologue to libp2p-webrtc-noise:<FINGERPRINTS> before starting the actual Noise handshake.
// <FINGERPRINTS> is the concatenation of the of the two TLS fingerprints of A and B in their multihash byte representation, sorted in ascending order.
const fingerprintsPrologue = this.generateNoisePrologue(peerConnection, remoteCerthash.code, ma)
// Since we use the default crypto interface and do not use a static key or early data,
// we pass in undefined for these parameters.
const noise = Noise({ prologueBytes: fingerprintsPrologue })()
// Since we use the default crypto interface and do not use a static key or early data,
// we pass in undefined for these parameters.
const noise = Noise({ prologueBytes: fingerprintsPrologue })()
const wrappedChannel = createStream({ channel: handshakeDataChannel, direction: 'inbound', dataChannelOptions: this.init.dataChannel })
const wrappedDuplex = {
...wrappedChannel,
sink: wrappedChannel.sink.bind(wrappedChannel),
source: (async function * () {
for await (const list of wrappedChannel.source) {
for (const buf of list) {
yield buf
const wrappedChannel = createStream({ channel: handshakeDataChannel, direction: 'inbound', dataChannelOptions: this.init.dataChannel })
const wrappedDuplex = {
...wrappedChannel,
sink: wrappedChannel.sink.bind(wrappedChannel),
source: (async function * () {
for await (const list of wrappedChannel.source) {
for (const buf of list) {
yield buf
}
}
}
}())
}
}())
}
// Creating the connection before completion of the noise
// handshake ensures that the stream opening callback is set up
const maConn = new WebRTCMultiaddrConnection({
peerConnection,
remoteAddr: ma,
timeline: {
open: Date.now()
},
metrics: this.metrics?.dialerEvents
})
// Creating the connection before completion of the noise
// handshake ensures that the stream opening callback is set up
const maConn = new WebRTCMultiaddrConnection({
peerConnection,
remoteAddr: ma,
timeline: {
open: Date.now()
},
metrics: this.metrics?.dialerEvents
})
const eventListeningName = isFirefox ? 'iceconnectionstatechange' : 'connectionstatechange'
const eventListeningName = isFirefox ? 'iceconnectionstatechange' : 'connectionstatechange'
peerConnection.addEventListener(eventListeningName, () => {
switch (peerConnection.connectionState) {
case 'failed':
case 'disconnected':
case 'closed':
maConn.close().catch((err) => {
log.error('error closing connection', err)
}).finally(() => {
// Remove the event listener once the connection is closed
controller.abort()
})
break
default:
break
}
}, { signal })
peerConnection.addEventListener(eventListeningName, () => {
switch (peerConnection.connectionState) {
case 'failed':
case 'disconnected':
case 'closed':
maConn.close().catch((err) => {
log.error('error closing connection', err)
}).finally(() => {
// Remove the event listener once the connection is closed
controller.abort()
})
break
default:
break
}
}, { signal })
// Track opened peer connection
this.metrics?.dialerEvents.increment({ peer_connection: true })
// Track opened peer connection
this.metrics?.dialerEvents.increment({ peer_connection: true })
const muxerFactory = new DataChannelMuxerFactory({ peerConnection, metrics: this.metrics?.dialerEvents, dataChannelOptions: this.init.dataChannel })
const muxerFactory = new DataChannelMuxerFactory({ peerConnection, metrics: this.metrics?.dialerEvents, dataChannelOptions: this.init.dataChannel })
// For outbound connections, the remote is expected to start the noise handshake.
// Therefore, we need to secure an inbound noise connection from the remote.
await noise.secureInbound(myPeerId, wrappedDuplex, theirPeerId)
// For outbound connections, the remote is expected to start the noise handshake.
// Therefore, we need to secure an inbound noise connection from the remote.
await noise.secureInbound(myPeerId, wrappedDuplex, theirPeerId)
return options.upgrader.upgradeOutbound(maConn, { skipProtection: true, skipEncryption: true, muxerFactory })
return await options.upgrader.upgradeOutbound(maConn, { skipProtection: true, skipEncryption: true, muxerFactory })
} catch (err) {
peerConnection.close()
throw err
}
}

@@ -250,0 +256,0 @@

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc