UNPKG

libp2p

Version:

JavaScript implementation of libp2p, a modular peer to peer network stack

380 lines • 16.2 kB
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