UNPKG

libp2p

Version:

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

391 lines (307 loc) • 12.6 kB
import { connectionSymbol, LimitedConnectionError, ConnectionClosedError, ConnectionClosingError, TooManyOutboundProtocolStreamsError, TooManyInboundProtocolStreamsError } from '@libp2p/interface' import * as mss from '@libp2p/multistream-select' import { setMaxListeners } from 'main-event' import { PROTOCOL_NEGOTIATION_TIMEOUT } from './connection-manager/constants.defaults.ts' import { MuxerUnavailableError } from './errors.ts' import { DEFAULT_MAX_INBOUND_STREAMS, DEFAULT_MAX_OUTBOUND_STREAMS } from './registrar.ts' import type { AbortOptions, Logger, Direction, Connection as ConnectionInterface, Stream, ConnectionTimeline, ConnectionStatus, NewStreamOptions, PeerId, ConnectionLimits, StreamMuxerFactory, StreamMuxer, Metrics, PeerStore, MultiaddrConnection } from '@libp2p/interface' import type { Registrar } from '@libp2p/interface-internal' import type { Multiaddr } from '@multiformats/multiaddr' const CLOSE_TIMEOUT = 500 export interface ConnectionComponents { peerStore: PeerStore registrar: Registrar metrics?: Metrics } export interface ConnectionInit { id: string maConn: MultiaddrConnection remotePeer: PeerId direction?: Direction muxerFactory?: StreamMuxerFactory encryption?: string limits?: ConnectionLimits outboundStreamProtocolNegotiationTimeout?: number inboundStreamProtocolNegotiationTimeout?: number } /** * An implementation of the js-libp2p connection. * Any libp2p transport should use an upgrader to return this connection. */ export class Connection implements ConnectionInterface { public readonly id: string public readonly remoteAddr: Multiaddr public readonly remotePeer: PeerId public direction: Direction public timeline: ConnectionTimeline public multiplexer?: string public encryption?: string public status: ConnectionStatus public limits?: ConnectionLimits public readonly log: Logger public tags: string[] private readonly maConn: MultiaddrConnection private readonly muxer?: StreamMuxer private readonly components: ConnectionComponents private readonly outboundStreamProtocolNegotiationTimeout: number private readonly inboundStreamProtocolNegotiationTimeout: number constructor (components: ConnectionComponents, init: ConnectionInit) { this.components = components this.id = init.id this.remoteAddr = init.maConn.remoteAddr this.remotePeer = init.remotePeer this.direction = init.direction ?? 'outbound' this.status = 'open' this.timeline = init.maConn.timeline this.encryption = init.encryption this.limits = init.limits this.maConn = init.maConn this.log = init.maConn.log this.outboundStreamProtocolNegotiationTimeout = init.outboundStreamProtocolNegotiationTimeout ?? PROTOCOL_NEGOTIATION_TIMEOUT this.inboundStreamProtocolNegotiationTimeout = init.inboundStreamProtocolNegotiationTimeout ?? PROTOCOL_NEGOTIATION_TIMEOUT if (this.remoteAddr.getPeerId() == null) { this.remoteAddr = this.remoteAddr.encapsulate(`/p2p/${this.remotePeer}`) } this.tags = [] if (init.muxerFactory != null) { this.multiplexer = init.muxerFactory.protocol this.muxer = init.muxerFactory.createStreamMuxer({ direction: this.direction, log: this.log, // Run anytime a remote stream is created onIncomingStream: (stream) => { this.onIncomingStream(stream) } }) // Pipe all data through the muxer void Promise.all([ this.muxer.sink(this.maConn.source), this.maConn.sink(this.muxer.source) ]).catch(err => { this.log.error('error piping data through muxer - %e', err) }) } } readonly [Symbol.toStringTag] = 'Connection' readonly [connectionSymbol] = true get streams (): Stream[] { return this.muxer?.streams ?? [] } /** * Create a new stream over this connection */ newStream = async (protocols: string[], options: NewStreamOptions = {}): Promise<Stream> => { if (this.status === 'closing') { throw new ConnectionClosingError('the connection is being closed') } if (this.status === 'closed') { throw new ConnectionClosedError('the connection is closed') } if (!Array.isArray(protocols)) { protocols = [protocols] } if (this.limits != null && options?.runOnLimitedConnection !== true) { throw new LimitedConnectionError('Cannot open protocol stream on limited connection') } if (this.muxer == null) { throw new MuxerUnavailableError('Connection is not multiplexed') } this.log.trace('starting new stream for protocols %s', protocols) const muxedStream = await this.muxer.newStream() this.log.trace('started new stream %s for protocols %s', muxedStream.id, protocols) try { if (options.signal == null) { muxedStream.log('no abort signal was passed while trying to negotiate protocols %s falling back to default timeout', protocols) const signal = AbortSignal.timeout(this.outboundStreamProtocolNegotiationTimeout) setMaxListeners(Infinity, signal) options = { ...options, signal } } muxedStream.log.trace('selecting protocol from protocols %s', protocols) const { stream, protocol } = await mss.select(muxedStream, protocols, { ...options, log: muxedStream.log, yieldBytes: true }) muxedStream.log('selected protocol %s', protocol) const outgoingLimit = findOutgoingStreamLimit(protocol, this.components.registrar, options) const streamCount = countStreams(protocol, 'outbound', this) if (streamCount >= outgoingLimit) { const err = new TooManyOutboundProtocolStreamsError(`Too many outbound protocol streams for protocol "${protocol}" - ${streamCount}/${outgoingLimit}`) muxedStream.abort(err) throw err } // If a protocol stream has been successfully negotiated and is to be passed to the application, // the peer store should ensure that the peer is registered with that protocol await this.components.peerStore.merge(this.remotePeer, { protocols: [protocol] }) // after the handshake the returned stream can have early data so override // the source/sink muxedStream.source = stream.source muxedStream.sink = stream.sink muxedStream.protocol = protocol // allow closing the write end of a not-yet-negotiated stream if (stream.closeWrite != null) { muxedStream.closeWrite = stream.closeWrite } // allow closing the read end of a not-yet-negotiated stream if (stream.closeRead != null) { muxedStream.closeRead = stream.closeRead } // make sure we don't try to negotiate a stream we are closing if (stream.close != null) { muxedStream.close = stream.close } this.components.metrics?.trackProtocolStream(muxedStream, this) muxedStream.direction = 'outbound' return muxedStream } catch (err: any) { this.log.error('could not create new outbound stream on connection %s %a for protocols %s - %e', this.direction === 'inbound' ? 'from' : 'to', this.remoteAddr, protocols, err) if (muxedStream.timeline.close == null) { muxedStream.abort(err) } throw err } } private onIncomingStream (muxedStream: Stream): void { const signal = AbortSignal.timeout(this.inboundStreamProtocolNegotiationTimeout) setMaxListeners(Infinity, signal) void Promise.resolve() .then(async () => { const protocols = this.components.registrar.getProtocols() const { stream, protocol } = await mss.handle(muxedStream, protocols, { signal, log: muxedStream.log, yieldBytes: false }) this.log('incoming %s stream opened', protocol) const incomingLimit = findIncomingStreamLimit(protocol, this.components.registrar) const streamCount = countStreams(protocol, 'inbound', this) if (streamCount === incomingLimit) { const err = new TooManyInboundProtocolStreamsError(`Too many inbound protocol streams for protocol "${protocol}" - limit ${incomingLimit}`) muxedStream.abort(err) throw err } // after the handshake the returned stream can have early data so override // the source/sink muxedStream.source = stream.source muxedStream.sink = stream.sink muxedStream.protocol = protocol // allow closing the write end of a not-yet-negotiated stream if (stream.closeWrite != null) { muxedStream.closeWrite = stream.closeWrite } // allow closing the read end of a not-yet-negotiated stream if (stream.closeRead != null) { muxedStream.closeRead = stream.closeRead } // make sure we don't try to negotiate a stream we are closing if (stream.close != null) { muxedStream.close = stream.close } // If a protocol stream has been successfully negotiated and is to be passed to the application, // the peer store should ensure that the peer is registered with that protocol await this.components.peerStore.merge(this.remotePeer, { protocols: [protocol] }, { signal }) this.components.metrics?.trackProtocolStream(muxedStream, this) const { handler, options } = this.components.registrar.getHandler(protocol) if (this.limits != null && options.runOnLimitedConnection !== true) { throw new LimitedConnectionError('Cannot open protocol stream on limited connection') } await handler({ connection: this, stream: muxedStream }) }) .catch(async err => { this.log.error('error handling incoming stream id %s - %e', muxedStream.id, err) muxedStream.abort(err) }) } /** * Close the connection */ async close (options: AbortOptions = {}): Promise<void> { if (this.status === 'closed' || this.status === 'closing') { return } this.log('closing connection to %a', this.remoteAddr) this.status = 'closing' if (options.signal == null) { const signal = AbortSignal.timeout(CLOSE_TIMEOUT) setMaxListeners(Infinity, signal) options = { ...options, signal } } try { this.log.trace('closing underlying transport') // ensure remaining streams are closed gracefully await this.muxer?.close(options) // close the underlying transport await this.maConn.close(options) this.log.trace('updating timeline with close time') this.status = 'closed' this.timeline.close = Date.now() } catch (err: any) { this.log.error('error encountered during graceful close of connection to %a', this.remoteAddr, err) this.abort(err) } } abort (err: Error): void { if (this.status === 'closed') { return } this.log.error('aborting connection to %a due to error', this.remoteAddr, err) this.status = 'closing' // ensure remaining streams are aborted this.muxer?.abort(err) // abort the underlying transport this.maConn.abort(err) this.status = 'closed' this.timeline.close = Date.now() } } export function createConnection (components: ConnectionComponents, init: ConnectionInit): ConnectionInterface { return new Connection(components, init) } function findIncomingStreamLimit (protocol: string, registrar: Registrar): number | undefined { try { const { options } = registrar.getHandler(protocol) return options.maxInboundStreams } catch (err: any) { if (err.name !== 'UnhandledProtocolError') { throw err } } return DEFAULT_MAX_INBOUND_STREAMS } function findOutgoingStreamLimit (protocol: string, registrar: Registrar, options: NewStreamOptions = {}): number { try { const { options } = registrar.getHandler(protocol) if (options.maxOutboundStreams != null) { return options.maxOutboundStreams } } catch (err: any) { if (err.name !== 'UnhandledProtocolError') { throw err } } return options.maxOutboundStreams ?? DEFAULT_MAX_OUTBOUND_STREAMS } function countStreams (protocol: string, direction: 'inbound' | 'outbound', connection: Connection): number { let streamCount = 0 connection.streams.forEach(stream => { if (stream.direction === direction && stream.protocol === protocol) { streamCount++ } }) return streamCount }