libp2p
Version:
JavaScript implementation of libp2p, a modular peer to peer network stack
380 lines • 16.2 kB
JavaScript
import { InvalidMultiaddrError, InvalidPeerIdError } from '@libp2p/interface';
import * as mss from '@libp2p/multistream-select';
import { peerIdFromString } from '@libp2p/peer-id';
import { trackedMap } from '@libp2p/utils/tracked-map';
import { anySignal } from 'any-signal';
import { setMaxListeners } from 'main-event';
import { CustomProgressEvent } from 'progress-events';
import { raceSignal } from 'race-signal';
import { PROTOCOL_NEGOTIATION_TIMEOUT, INBOUND_UPGRADE_TIMEOUT } from './connection-manager/constants.js';
import { createConnection } from './connection.js';
import { ConnectionDeniedError, ConnectionInterceptedError, EncryptionFailedError, MuxerUnavailableError } from './errors.js';
export class Upgrader {
components;
connectionEncrypters;
streamMuxers;
inboundUpgradeTimeout;
inboundStreamProtocolNegotiationTimeout;
outboundStreamProtocolNegotiationTimeout;
events;
metrics;
constructor(components, init) {
this.components = components;
this.connectionEncrypters = trackedMap({
name: 'libp2p_upgrader_connection_encrypters',
metrics: this.components.metrics
});
init.connectionEncrypters.forEach(encrypter => {
this.connectionEncrypters.set(encrypter.protocol, encrypter);
});
this.streamMuxers = trackedMap({
name: 'libp2p_upgrader_stream_multiplexers',
metrics: this.components.metrics
});
init.streamMuxers.forEach(muxer => {
this.streamMuxers.set(muxer.protocol, muxer);
});
this.inboundUpgradeTimeout = init.inboundUpgradeTimeout ?? INBOUND_UPGRADE_TIMEOUT;
this.inboundStreamProtocolNegotiationTimeout = init.inboundStreamProtocolNegotiationTimeout ?? PROTOCOL_NEGOTIATION_TIMEOUT;
this.outboundStreamProtocolNegotiationTimeout = init.outboundStreamProtocolNegotiationTimeout ?? PROTOCOL_NEGOTIATION_TIMEOUT;
this.events = components.events;
this.metrics = {
dials: components.metrics?.registerCounterGroup('libp2p_connection_manager_dials_total'),
errors: components.metrics?.registerCounterGroup('libp2p_connection_manager_dial_errors_total'),
inboundErrors: components.metrics?.registerCounterGroup('libp2p_connection_manager_dials_inbound_errors_total'),
outboundErrors: components.metrics?.registerCounterGroup('libp2p_connection_manager_dials_outbound_errors_total')
};
}
[Symbol.toStringTag] = '@libp2p/upgrader';
async shouldBlockConnection(method, ...args) {
const denyOperation = this.components.connectionGater[method];
if (denyOperation == null) {
return;
}
const result = await denyOperation.apply(this.components.connectionGater, args);
if (result === true) {
throw new ConnectionInterceptedError(`The multiaddr connection is blocked by gater.${method}`);
}
}
createInboundAbortSignal(signal) {
const output = anySignal([
AbortSignal.timeout(this.inboundUpgradeTimeout),
signal
]);
setMaxListeners(Infinity, output);
return output;
}
/**
* Upgrades an inbound connection
*/
async upgradeInbound(maConn, opts) {
let accepted = false;
// always apply upgrade timeout for incoming upgrades
const signal = this.createInboundAbortSignal(opts.signal);
try {
this.metrics.dials?.increment({
inbound: true
});
accepted = await raceSignal(this.components.connectionManager.acceptIncomingConnection(maConn), signal);
if (!accepted) {
throw new ConnectionDeniedError('Connection denied');
}
await raceSignal(this.shouldBlockConnection('denyInboundConnection', maConn), signal);
await this._performUpgrade(maConn, 'inbound', {
...opts,
signal
});
}
catch (err) {
this.metrics.errors?.increment({
inbound: true
});
this.metrics.inboundErrors?.increment({
[err.name ?? 'Error']: true
});
throw err;
}
finally {
signal.clear();
if (accepted) {
this.components.connectionManager.afterUpgradeInbound();
}
}
}
/**
* Upgrades an outbound connection
*/
async upgradeOutbound(maConn, opts) {
try {
this.metrics.dials?.increment({
outbound: true
});
const idStr = maConn.remoteAddr.getPeerId();
let remotePeerId;
if (idStr != null) {
remotePeerId = peerIdFromString(idStr);
await raceSignal(this.shouldBlockConnection('denyOutboundConnection', remotePeerId, maConn), opts.signal);
}
let direction = 'outbound';
// act as the multistream-select server if we are not to be the initiator
if (opts.initiator === false) {
direction = 'inbound';
}
return await this._performUpgrade(maConn, direction, opts);
}
catch (err) {
this.metrics.errors?.increment({
outbound: true
});
this.metrics.outboundErrors?.increment({
[err.name ?? 'Error']: true
});
throw err;
}
}
async _performUpgrade(maConn, direction, opts) {
let encryptedConn;
let remotePeer;
let upgradedConn;
let muxerFactory;
let cryptoProtocol;
const id = `${(parseInt(String(Math.random() * 1e9))).toString(36)}${Date.now()}`;
maConn.log = maConn.log.newScope(`${direction}:${id}`);
this.components.metrics?.trackMultiaddrConnection(maConn);
maConn.log.trace('starting the %s connection upgrade', direction);
// Protect
let protectedConn = maConn;
if (opts?.skipProtection !== true) {
const protector = this.components.connectionProtector;
if (protector != null) {
maConn.log('protecting the %s connection', direction);
protectedConn = await protector.protect(maConn, opts);
}
}
try {
// Encrypt the connection
encryptedConn = protectedConn;
if (opts?.skipEncryption !== true) {
opts?.onProgress?.(new CustomProgressEvent(`upgrader:encrypt-${direction}-connection`));
({
conn: encryptedConn,
remotePeer,
protocol: cryptoProtocol,
streamMuxer: muxerFactory
} = await (direction === 'inbound'
? this._encryptInbound(protectedConn, opts)
: this._encryptOutbound(protectedConn, opts)));
const maConn = {
...protectedConn,
...encryptedConn
};
await this.shouldBlockConnection(direction === 'inbound' ? 'denyInboundEncryptedConnection' : 'denyOutboundEncryptedConnection', remotePeer, maConn);
}
else {
const idStr = maConn.remoteAddr.getPeerId();
if (idStr == null) {
throw new InvalidMultiaddrError(`${direction} connection that skipped encryption must have a peer id`);
}
const remotePeerId = peerIdFromString(idStr);
cryptoProtocol = 'native';
remotePeer = remotePeerId;
}
// this can happen if we dial a multiaddr without a peer id, we only find
// out the identity of the remote after the connection is encrypted
if (remotePeer.equals(this.components.peerId)) {
const err = new InvalidPeerIdError('Can not dial self');
maConn.abort(err);
throw err;
}
upgradedConn = encryptedConn;
if (opts?.muxerFactory != null) {
muxerFactory = opts.muxerFactory;
}
else if (muxerFactory == null && this.streamMuxers.size > 0) {
opts?.onProgress?.(new CustomProgressEvent(`upgrader:multiplex-${direction}-connection`));
// Multiplex the connection
const multiplexed = await (direction === 'inbound'
? this._multiplexInbound({
...protectedConn,
...encryptedConn
}, this.streamMuxers, opts)
: this._multiplexOutbound({
...protectedConn,
...encryptedConn
}, this.streamMuxers, opts));
muxerFactory = multiplexed.muxerFactory;
upgradedConn = multiplexed.stream;
}
}
catch (err) {
maConn.log.error('failed to upgrade inbound connection %s %a - %e', direction === 'inbound' ? 'from' : 'to', maConn.remoteAddr, err);
throw err;
}
await this.shouldBlockConnection(direction === 'inbound' ? 'denyInboundUpgradedConnection' : 'denyOutboundUpgradedConnection', remotePeer, maConn);
const conn = this._createConnection({
id,
cryptoProtocol,
direction,
maConn,
upgradedConn,
muxerFactory,
remotePeer,
limits: opts?.limits
});
conn.log('successfully upgraded %s connection', direction);
return conn;
}
/**
* A convenience method for generating a new `Connection`
*/
_createConnection(opts) {
const { id, cryptoProtocol, direction, maConn, upgradedConn, remotePeer, muxerFactory, limits } = opts;
let connection; // eslint-disable-line prefer-const
const _timeline = maConn.timeline;
maConn.timeline = new Proxy(_timeline, {
set: (...args) => {
if (args[1] === 'close' && args[2] != null && _timeline.close == null) {
// Wait for close to finish before notifying of the closure
(async () => {
try {
if (connection.status === 'open') {
await connection.close();
}
}
catch (err) {
connection.log.error('error closing connection after timeline close %e', err);
}
finally {
this.events.safeDispatchEvent('connection:close', {
detail: connection
});
}
})().catch(err => {
connection.log.error('error thrown while dispatching connection:close event %e', err);
});
}
return Reflect.set(...args);
}
});
maConn.timeline.upgraded = Date.now();
// Create the connection
connection = createConnection(this.components, {
id,
maConn: upgradedConn,
remotePeer,
direction,
muxerFactory,
encryption: cryptoProtocol,
limits,
outboundStreamProtocolNegotiationTimeout: this.outboundStreamProtocolNegotiationTimeout,
inboundStreamProtocolNegotiationTimeout: this.inboundStreamProtocolNegotiationTimeout
});
this.events.safeDispatchEvent('connection:open', {
detail: connection
});
return connection;
}
/**
* Attempts to encrypt the incoming `connection` with the provided `cryptos`
*/
async _encryptInbound(connection, options) {
const protocols = Array.from(this.connectionEncrypters.keys());
try {
const { stream, protocol } = await mss.handle(connection, protocols, {
...options,
log: connection.log
});
const encrypter = this.connectionEncrypters.get(protocol);
if (encrypter == null) {
throw new EncryptionFailedError(`no crypto module found for ${protocol}`);
}
connection.log('encrypting inbound connection to %a using %s', connection.remoteAddr, protocol);
return {
...await encrypter.secureInbound(stream, options),
protocol
};
}
catch (err) {
connection.log.error('encrypting inbound connection from %a failed', connection.remoteAddr, err);
throw new EncryptionFailedError(err.message);
}
}
/**
* Attempts to encrypt the given `connection` with the provided connection encrypters.
* The first `ConnectionEncrypter` module to succeed will be used
*/
async _encryptOutbound(connection, options) {
const protocols = Array.from(this.connectionEncrypters.keys());
try {
connection.log.trace('selecting encrypter from %s', protocols);
const { stream, protocol } = await mss.select(connection, protocols, {
...options,
log: connection.log,
yieldBytes: true
});
const encrypter = this.connectionEncrypters.get(protocol);
if (encrypter == null) {
throw new EncryptionFailedError(`no crypto module found for ${protocol}`);
}
connection.log('encrypting outbound connection to %a using %s', connection.remoteAddr, protocol);
return {
...await encrypter.secureOutbound(stream, options),
protocol
};
}
catch (err) {
connection.log.error('encrypting outbound connection to %a failed', connection.remoteAddr, err);
throw new EncryptionFailedError(err.message);
}
}
/**
* Selects one of the given muxers via multistream-select. That
* muxer will be used for all future streams on the connection.
*/
async _multiplexOutbound(connection, muxers, options) {
const protocols = Array.from(muxers.keys());
connection.log('outbound selecting muxer %s', protocols);
try {
connection.log.trace('selecting stream muxer from %s', protocols);
const { stream, protocol } = await mss.select(connection, protocols, {
...options,
log: connection.log,
yieldBytes: true
});
connection.log('selected %s as muxer protocol', protocol);
const muxerFactory = muxers.get(protocol);
return { stream, muxerFactory };
}
catch (err) {
connection.log.error('error multiplexing outbound connection', err);
throw new MuxerUnavailableError(String(err));
}
}
/**
* Registers support for one of the given muxers via multistream-select. The
* selected muxer will be used for all future streams on the connection.
*/
async _multiplexInbound(connection, muxers, options) {
const protocols = Array.from(muxers.keys());
connection.log('inbound handling muxers %s', protocols);
try {
const { stream, protocol } = await mss.handle(connection, protocols, {
...options,
log: connection.log
});
const muxerFactory = muxers.get(protocol);
return { stream, muxerFactory };
}
catch (err) {
connection.log.error('error multiplexing inbound connection', err);
throw new MuxerUnavailableError(String(err));
}
}
getConnectionEncrypters() {
return this.connectionEncrypters;
}
getStreamMuxers() {
return this.streamMuxers;
}
}
//# sourceMappingURL=upgrader.js.map