Skip to content
This repository has been archived by the owner on Oct 30, 2023. It is now read-only.

fix: send user agent header with libp2p version #12

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .aegir.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,40 @@ import body from 'body-parser'
export default {
test: {
before: async () => {
let lastRequest = {
headers: {},
params: {},
body: ''
}

const providers = new Map()
const echoServer = new EchoServer()
echoServer.polka.use(body.text())
echoServer.polka.post('/add-providers/:cid', (req, res) => {
lastRequest = {
headers: req.headers,
params: req.params,
body: req.body
}

providers.set(req.params.cid, req.body)
res.end()
})
echoServer.polka.get('/cid/:cid', (req, res) => {
lastRequest = {
headers: req.headers,
params: req.params,
body: req.body
}

const provs = providers.get(req.params.cid) ?? '[]'
providers.delete(req.params.cid)

res.end(provs)
})
echoServer.polka.get('/last-request', (req, res) => {
res.end(JSON.stringify(lastRequest))
})

await echoServer.start()

Expand Down
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,9 @@
},
"dependencies": {
"@libp2p/interface-content-routing": "^2.0.2",
"@libp2p/interface-peer-id": "^2.0.2",
"@libp2p/interface-peer-info": "^1.0.9",
"@libp2p/interface-peer-store": "^2.0.3",
"@libp2p/interfaces": "^3.3.1",
"@libp2p/logger": "^2.0.7",
"@libp2p/peer-id": "^2.0.3",
Expand All @@ -147,12 +149,14 @@
"iterable-ndjson": "^1.1.0",
"multiformats": "^11.0.2",
"p-defer": "^4.0.0",
"p-queue": "^7.3.4"
"p-queue": "^7.3.4",
"sinon-ts": "^1.0.0"
},
"devDependencies": {
"@libp2p/peer-id-factory": "^2.0.3",
"aegir": "^39.0.7",
"body-parser": "^1.20.2",
"it-all": "^3.0.1"
"it-all": "^3.0.1",
"it-drain": "^3.0.2"
}
}
41 changes: 37 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import ndjson from 'iterable-ndjson'
import defer from 'p-defer'
import PQueue from 'p-queue'
import type { ContentRouting } from '@libp2p/interface-content-routing'
import type { PeerId } from '@libp2p/interface-peer-id'
import type { PeerInfo } from '@libp2p/interface-peer-info'
import type { PeerStore } from '@libp2p/interface-peer-store'
import type { AbortOptions } from '@libp2p/interfaces'
import type { Startable } from '@libp2p/interfaces/startable'
import type { Multiaddr } from '@multiformats/multiaddr'
Expand Down Expand Up @@ -40,6 +42,11 @@ export interface IpniContentRoutingInit {
timeout?: number
}

export interface IpniContentRoutingComponents {
peerId: PeerId
peerStore: PeerStore
}

const defaultValues = {
concurrentRequests: 4,
timeout: 30e3
Expand All @@ -54,11 +61,14 @@ class IpniContentRouting implements ContentRouting, Startable {
private readonly shutDownController: AbortController
private readonly clientUrl: URL
private readonly timeout: number
private readonly peerId: PeerId
private readonly peerStore: PeerStore
private agentVersion?: string

/**
* Create a new DelegatedContentRouting instance
*/
constructor (url: string | URL, init: IpniContentRoutingInit = {}) {
constructor (url: string | URL, init: IpniContentRoutingInit = {}, components: IpniContentRoutingComponents) {
log('enabled IPNI routing via', url)
this.started = false
this.shutDownController = new AbortController()
Expand All @@ -67,6 +77,8 @@ class IpniContentRouting implements ContentRouting, Startable {
})
this.clientUrl = url instanceof URL ? url : new URL(url)
this.timeout = init.timeout ?? defaultValues.timeout
this.peerId = components.peerId
this.peerStore = components.peerStore
}

isStarted (): boolean {
Expand All @@ -83,6 +95,21 @@ class IpniContentRouting implements ContentRouting, Startable {
this.started = false
}

private async getAgentVersion (): Promise<string> {
if (this.agentVersion == null) {
const peer = await this.peerStore.get(this.peerId)
const agentVersionBuf = peer.metadata.get('AgentVersion')

if (agentVersionBuf != null) {
this.agentVersion = new TextDecoder().decode(agentVersionBuf)
} else {
this.agentVersion = ''
}
}

return this.agentVersion
}

async * findProviders (key: CID, options: AbortOptions = {}): AsyncIterable<PeerInfo> {
log('findProviders starts: %c', key)

Expand All @@ -99,7 +126,13 @@ class IpniContentRouting implements ContentRouting, Startable {
await onStart.promise

const resource = `${this.clientUrl}cid/${key.toString()}?cascade=ipfs-dht`
const getOptions = { headers: { Accept: 'application/x-ndjson' }, signal }
const getOptions = {
headers: {
Accept: 'application/x-ndjson',
'User-Agent': await this.getAgentVersion()
},
signal
}
const a = await fetch(resource, getOptions)

if (a.body == null) {
Expand Down Expand Up @@ -153,6 +186,6 @@ class IpniContentRouting implements ContentRouting, Startable {
}
}

export function ipniContentRouting (url: string | URL, init: IpniContentRoutingInit = {}): () => ContentRouting {
return () => new IpniContentRouting(url, init)
export function ipniContentRouting (url: string | URL, init: IpniContentRoutingInit = {}): (components: IpniContentRoutingComponents) => ContentRouting {
return (components: IpniContentRoutingComponents) => new IpniContentRouting(url, init, components)
}
67 changes: 63 additions & 4 deletions test/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@
import { createEd25519PeerId } from '@libp2p/peer-id-factory'
import { expect } from 'aegir/chai'
import all from 'it-all'
import drain from 'it-drain'
import { CID } from 'multiformats/cid'
import { type StubbedInstance, stubInterface } from 'sinon-ts'
import { ipniContentRouting } from '../src/index.js'
import type { PeerId } from '@libp2p/interface-peer-id'
import type { PeerStore } from '@libp2p/interface-peer-store'

if (process.env.ECHO_SERVER == null) {
throw new Error('Echo server not configured correctly')
Expand All @@ -13,6 +17,17 @@ if (process.env.ECHO_SERVER == null) {
const serverUrl = process.env.ECHO_SERVER

describe('IPNIContentRouting', function () {
let peerId: PeerId
let peerStore: StubbedInstance<PeerStore>

beforeEach(async () => {
peerId = await createEd25519PeerId()
peerStore = stubInterface<PeerStore>()
peerStore.get.withArgs(peerId).resolves({
metadata: new Map()
})
})

it('should find providers', async () => {
const providers = [{
Metadata: 'gBI=',
Expand All @@ -38,7 +53,10 @@ describe('IPNIContentRouting', function () {
body: providers.map(prov => JSON.stringify(prov)).join('\n')
})

const routing = ipniContentRouting(serverUrl)()
const routing = ipniContentRouting(serverUrl)({
peerId,
peerStore
})

const provs = await all(routing.findProviders(cid))
expect(provs.map(prov => ({
Expand All @@ -59,7 +77,10 @@ describe('IPNIContentRouting', function () {
body: 'not json'
})

const routing = ipniContentRouting(serverUrl)()
const routing = ipniContentRouting(serverUrl)({
peerId,
peerStore
})

const provs = await all(routing.findProviders(cid))
expect(provs).to.be.empty()
Expand Down Expand Up @@ -87,17 +108,55 @@ describe('IPNIContentRouting', function () {
body: providers.map(prov => JSON.stringify(prov)).join('\n')
})

const routing = ipniContentRouting(serverUrl)()
const routing = ipniContentRouting(serverUrl)({
peerId,
peerStore
})

const provs = await all(routing.findProviders(cid))
expect(provs).to.be.empty()
})

it('should handle empty input', async () => {
const cid = CID.parse('QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn')
const routing = ipniContentRouting(serverUrl)()
const routing = ipniContentRouting(serverUrl)({
peerId,
peerStore
})

const provs = await all(routing.findProviders(cid))
expect(provs).to.be.empty()
})

it('should send user agent header', async () => {
const agentVersion = 'herp/1.0.0 derp/1.0.0'

// return user agent
peerStore.get.withArgs(peerId).resolves({
metadata: new Map([['AgentVersion', new TextEncoder().encode(agentVersion)]])
})

const cid = CID.parse('QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn')

// load providers for the router to fetch
await fetch(`${process.env.ECHO_SERVER}/add-providers/${cid.toString()}`, {
method: 'POST',
body: '[]\n'
})

const routing = ipniContentRouting(serverUrl)({
peerId,
peerStore
})

await drain(routing.findProviders(cid))

const response = await fetch(`${process.env.ECHO_SERVER}/last-request`, {
method: 'GET'
})
const bodyText = await response.text()
const body = JSON.parse(bodyText)

expect(body).to.have.nested.property('headers.user-agent', agentVersion)
})
})