mirror of
https://github.com/fluencelabs/js-libp2p
synced 2025-03-28 05:11:04 +00:00
There are a few places in the codebase where we send/receive data from the network without timeouts/abort controllers which means the user has to wait for the underlying socket to timeout which can take a long time depending on the platform, if at all. This change ensures we can time out while running identify (both flavours), ping and fetch and adds tests to ensure there are no regressions.
274 lines
9.3 KiB
TypeScript
274 lines
9.3 KiB
TypeScript
/* eslint-env mocha */
|
|
|
|
import { expect } from 'aegir/chai'
|
|
import sinon from 'sinon'
|
|
import { Multiaddr } from '@multiformats/multiaddr'
|
|
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
|
|
import { codes } from '../../src/errors.js'
|
|
import { IdentifyService, Message } from '../../src/identify/index.js'
|
|
import Peers from '../fixtures/peers.js'
|
|
import { PersistentPeerStore } from '@libp2p/peer-store'
|
|
import { DefaultAddressManager } from '../../src/address-manager/index.js'
|
|
import { MemoryDatastore } from 'datastore-core/memory'
|
|
import * as lp from 'it-length-prefixed'
|
|
import drain from 'it-drain'
|
|
import { pipe } from 'it-pipe'
|
|
import { mockConnectionGater, mockRegistrar, mockUpgrader, connectionPair } from '@libp2p/interface-compliance-tests/mocks'
|
|
import { createFromJSON } from '@libp2p/peer-id-factory'
|
|
import { Components } from '@libp2p/interfaces/components'
|
|
import { PeerRecordUpdater } from '../../src/peer-record-updater.js'
|
|
import {
|
|
MULTICODEC_IDENTIFY,
|
|
MULTICODEC_IDENTIFY_PUSH
|
|
} from '../../src/identify/consts.js'
|
|
import { DefaultConnectionManager } from '../../src/connection-manager/index.js'
|
|
import { DefaultTransportManager } from '../../src/transport-manager.js'
|
|
import delay from 'delay'
|
|
import { start, stop } from '@libp2p/interfaces/startable'
|
|
import { TimeoutController } from 'timeout-abort-controller'
|
|
|
|
const listenMaddrs = [new Multiaddr('/ip4/127.0.0.1/tcp/15002/ws')]
|
|
|
|
const defaultInit = {
|
|
protocolPrefix: 'ipfs',
|
|
host: {
|
|
agentVersion: 'v1.0.0'
|
|
}
|
|
}
|
|
|
|
const protocols = [MULTICODEC_IDENTIFY, MULTICODEC_IDENTIFY_PUSH]
|
|
|
|
async function createComponents (index: number) {
|
|
const peerId = await createFromJSON(Peers[index])
|
|
|
|
const components = new Components({
|
|
peerId,
|
|
datastore: new MemoryDatastore(),
|
|
registrar: mockRegistrar(),
|
|
upgrader: mockUpgrader(),
|
|
connectionGater: mockConnectionGater(),
|
|
peerStore: new PersistentPeerStore(),
|
|
connectionManager: new DefaultConnectionManager({
|
|
minConnections: 50,
|
|
maxConnections: 1000,
|
|
autoDialInterval: 1000
|
|
})
|
|
})
|
|
components.setAddressManager(new DefaultAddressManager(components, {
|
|
announce: listenMaddrs.map(ma => ma.toString())
|
|
}))
|
|
|
|
const transportManager = new DefaultTransportManager(components)
|
|
components.setTransportManager(transportManager)
|
|
|
|
await components.getPeerStore().protoBook.set(peerId, protocols)
|
|
|
|
return components
|
|
}
|
|
|
|
describe('identify', () => {
|
|
let localComponents: Components
|
|
let remoteComponents: Components
|
|
|
|
let remotePeerRecordUpdater: PeerRecordUpdater
|
|
|
|
beforeEach(async () => {
|
|
localComponents = await createComponents(0)
|
|
remoteComponents = await createComponents(1)
|
|
|
|
remotePeerRecordUpdater = new PeerRecordUpdater(remoteComponents)
|
|
|
|
await Promise.all([
|
|
start(localComponents),
|
|
start(remoteComponents)
|
|
])
|
|
})
|
|
|
|
afterEach(async () => {
|
|
sinon.restore()
|
|
|
|
await Promise.all([
|
|
stop(localComponents),
|
|
stop(remoteComponents)
|
|
])
|
|
})
|
|
|
|
it('should be able to identify another peer', async () => {
|
|
const localIdentify = new IdentifyService(localComponents, defaultInit)
|
|
const remoteIdentify = new IdentifyService(remoteComponents, defaultInit)
|
|
|
|
await start(localIdentify)
|
|
await start(remoteIdentify)
|
|
|
|
const [localToRemote] = connectionPair(localComponents, remoteComponents)
|
|
|
|
const localAddressBookConsumePeerRecordSpy = sinon.spy(localComponents.getPeerStore().addressBook, 'consumePeerRecord')
|
|
const localProtoBookSetSpy = sinon.spy(localComponents.getPeerStore().protoBook, 'set')
|
|
|
|
// Make sure the remote peer has a peer record to share during identify
|
|
await remotePeerRecordUpdater.update()
|
|
|
|
// Run identify
|
|
await localIdentify.identify(localToRemote)
|
|
|
|
expect(localAddressBookConsumePeerRecordSpy.callCount).to.equal(1)
|
|
expect(localProtoBookSetSpy.callCount).to.equal(1)
|
|
|
|
// Validate the remote peer gets updated in the peer store
|
|
const addresses = await localComponents.getPeerStore().addressBook.get(remoteComponents.getPeerId())
|
|
expect(addresses).to.exist()
|
|
|
|
expect(addresses).have.lengthOf(listenMaddrs.length)
|
|
expect(addresses.map((a) => a.multiaddr)[0].equals(listenMaddrs[0]))
|
|
expect(addresses.map((a) => a.isCertified)[0]).to.be.true()
|
|
})
|
|
|
|
// LEGACY
|
|
it('should be able to identify another peer with no certified peer records support', async () => {
|
|
const agentVersion = 'js-libp2p/5.0.0'
|
|
const localIdentify = new IdentifyService(localComponents, {
|
|
protocolPrefix: 'ipfs',
|
|
host: {
|
|
agentVersion: agentVersion
|
|
}
|
|
})
|
|
await start(localIdentify)
|
|
const remoteIdentify = new IdentifyService(remoteComponents, {
|
|
protocolPrefix: 'ipfs',
|
|
host: {
|
|
agentVersion: agentVersion
|
|
}
|
|
})
|
|
await start(remoteIdentify)
|
|
|
|
const [localToRemote] = connectionPair(localComponents, remoteComponents)
|
|
|
|
sinon.stub(localComponents.getPeerStore().addressBook, 'consumePeerRecord').throws()
|
|
|
|
const localProtoBookSetSpy = sinon.spy(localComponents.getPeerStore().protoBook, 'set')
|
|
|
|
// Run identify
|
|
await localIdentify.identify(localToRemote)
|
|
|
|
expect(localProtoBookSetSpy.callCount).to.equal(1)
|
|
|
|
// Validate the remote peer gets updated in the peer store
|
|
const addresses = await localComponents.getPeerStore().addressBook.get(remoteComponents.getPeerId())
|
|
expect(addresses).to.exist()
|
|
|
|
expect(addresses).have.lengthOf(listenMaddrs.length)
|
|
expect(addresses.map((a) => a.multiaddr)[0].equals(listenMaddrs[0]))
|
|
expect(addresses.map((a) => a.isCertified)[0]).to.be.false()
|
|
})
|
|
|
|
it('should throw if identified peer is the wrong peer', async () => {
|
|
const localIdentify = new IdentifyService(localComponents, defaultInit)
|
|
const remoteIdentify = new IdentifyService(remoteComponents, defaultInit)
|
|
|
|
await start(localIdentify)
|
|
await start(remoteIdentify)
|
|
|
|
const [localToRemote] = connectionPair(localComponents, remoteComponents)
|
|
|
|
// send an invalid message
|
|
await remoteComponents.getRegistrar().unhandle(MULTICODEC_IDENTIFY)
|
|
await remoteComponents.getRegistrar().handle(MULTICODEC_IDENTIFY, (data) => {
|
|
void Promise.resolve().then(async () => {
|
|
const { connection, stream } = data
|
|
const signedPeerRecord = await remoteComponents.getPeerStore().addressBook.getRawEnvelope(remoteComponents.getPeerId())
|
|
|
|
const message = Message.Identify.encode({
|
|
protocolVersion: '123',
|
|
agentVersion: '123',
|
|
// send bad public key
|
|
publicKey: localComponents.getPeerId().publicKey ?? new Uint8Array(0),
|
|
listenAddrs: [],
|
|
signedPeerRecord,
|
|
observedAddr: connection.remoteAddr.bytes,
|
|
protocols: []
|
|
})
|
|
|
|
await pipe(
|
|
[message],
|
|
lp.encode(),
|
|
stream,
|
|
drain
|
|
)
|
|
})
|
|
})
|
|
|
|
// Run identify
|
|
await expect(localIdentify.identify(localToRemote))
|
|
.to.eventually.be.rejected()
|
|
.and.to.have.property('code', codes.ERR_INVALID_PEER)
|
|
})
|
|
|
|
it('should store own host data and protocol version into metadataBook on start', async () => {
|
|
const agentVersion = 'js-project/1.0.0'
|
|
const localIdentify = new IdentifyService(localComponents, {
|
|
protocolPrefix: 'ipfs',
|
|
host: {
|
|
agentVersion
|
|
}
|
|
})
|
|
|
|
await expect(localComponents.getPeerStore().metadataBook.getValue(localComponents.getPeerId(), 'AgentVersion'))
|
|
.to.eventually.be.undefined()
|
|
await expect(localComponents.getPeerStore().metadataBook.getValue(localComponents.getPeerId(), 'ProtocolVersion'))
|
|
.to.eventually.be.undefined()
|
|
|
|
await start(localIdentify)
|
|
|
|
await expect(localComponents.getPeerStore().metadataBook.getValue(localComponents.getPeerId(), 'AgentVersion'))
|
|
.to.eventually.deep.equal(uint8ArrayFromString(agentVersion))
|
|
await expect(localComponents.getPeerStore().metadataBook.getValue(localComponents.getPeerId(), 'ProtocolVersion'))
|
|
.to.eventually.be.ok()
|
|
|
|
await stop(localIdentify)
|
|
})
|
|
|
|
it('should time out during identify', async () => {
|
|
const localIdentify = new IdentifyService(localComponents, defaultInit)
|
|
const remoteIdentify = new IdentifyService(remoteComponents, defaultInit)
|
|
|
|
await start(localIdentify)
|
|
await start(remoteIdentify)
|
|
|
|
const [localToRemote] = connectionPair(localComponents, remoteComponents)
|
|
|
|
// replace existing handler with a really slow one
|
|
await remoteComponents.getRegistrar().unhandle(MULTICODEC_IDENTIFY)
|
|
await remoteComponents.getRegistrar().handle(MULTICODEC_IDENTIFY, ({ stream }) => {
|
|
void pipe(
|
|
stream,
|
|
async function * (source) {
|
|
// we receive no data in the identify protocol, we just send our data
|
|
await drain(source)
|
|
|
|
// longer than the timeout
|
|
await delay(1000)
|
|
|
|
yield new Uint8Array()
|
|
},
|
|
stream
|
|
)
|
|
})
|
|
|
|
const newStreamSpy = sinon.spy(localToRemote, 'newStream')
|
|
|
|
// 10 ms timeout
|
|
const timeoutController = new TimeoutController(10)
|
|
|
|
// Run identify
|
|
await expect(localIdentify.identify(localToRemote, {
|
|
signal: timeoutController.signal
|
|
}))
|
|
.to.eventually.be.rejected.with.property('code', 'ABORT_ERR')
|
|
|
|
// should have closed stream
|
|
expect(newStreamSpy).to.have.property('callCount', 1)
|
|
const { stream } = await newStreamSpy.getCall(0).returnValue
|
|
expect(stream).to.have.nested.property('timeline.close')
|
|
})
|
|
})
|