Skip to content
Merged
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
9 changes: 8 additions & 1 deletion packages/client/.aegir.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,18 @@ const options = {
test: {
before: async () => {
let callCount = 0
let lastCalledUrl = ''
const providers = new Map()
const peers = new Map()
const ipnsGet = new Map()
const ipnsPut = new Map()
const echo = new EchoServer()
echo.polka.use(body.raw({ type: 'application/vnd.ipfs.ipns-record'}))
echo.polka.use(body.text())
echo.polka.use((req, res, next) => {
next()
lastCalledUrl = req.url
})
echo.polka.post('/add-providers/:cid', (req, res) => {
callCount++
providers.set(req.params.cid, req.body)
Expand All @@ -22,7 +27,6 @@ const options = {
callCount++
const records = providers.get(req.params.cid) ?? '[]'
providers.delete(req.params.cid)

res.end(records)
})
echo.polka.post('/add-peers/:peerId', (req, res) => {
Expand Down Expand Up @@ -68,6 +72,9 @@ const options = {
callCount = 0
res.end()
})
echo.polka.get('/last-called-url', (req, res) => {
res.end(lastCalledUrl)
})

await echo.start()

Expand Down
37 changes: 30 additions & 7 deletions packages/client/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import defer from 'p-defer'
import PQueue from 'p-queue'
import { BadResponseError, InvalidRequestError } from './errors.js'
import { DelegatedRoutingV1HttpApiClientContentRouting, DelegatedRoutingV1HttpApiClientPeerRouting } from './routings.js'
import type { DelegatedRoutingV1HttpApiClient, DelegatedRoutingV1HttpApiClientInit, GetIPNSOptions, PeerRecord } from './index.js'
import type { DelegatedRoutingV1HttpApiClient, DelegatedRoutingV1HttpApiClientInit, GetProvidersOptions, GetPeersOptions, GetIPNSOptions, PeerRecord } from './index.js'
import type { ContentRouting, PeerRouting, AbortOptions, PeerId } from '@libp2p/interface'
import type { Multiaddr } from '@multiformats/multiaddr'
import type { CID } from 'multiformats'
Expand All @@ -31,6 +31,8 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
private readonly timeout: number
private readonly contentRouting: ContentRouting
private readonly peerRouting: PeerRouting
private readonly filterAddrs?: string[]
private readonly filterProtocols?: string[]

/**
* Create a new DelegatedContentRouting instance
Expand All @@ -44,6 +46,8 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
})
this.clientUrl = url instanceof URL ? url : new URL(url)
this.timeout = init.timeout ?? defaultValues.timeout
this.filterAddrs = init.filterAddrs
this.filterProtocols = init.filterProtocols
this.contentRouting = new DelegatedRoutingV1HttpApiClientContentRouting(this)
this.peerRouting = new DelegatedRoutingV1HttpApiClientPeerRouting(this)
}
Expand All @@ -70,7 +74,7 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
this.started = false
}

async * getProviders (cid: CID, options: AbortOptions = {}): AsyncGenerator<PeerRecord> {
async * getProviders (cid: CID, options: GetProvidersOptions = {}): AsyncGenerator<PeerRecord> {
log('getProviders starts: %c', cid)

const timeoutSignal = AbortSignal.timeout(this.timeout)
Expand All @@ -88,9 +92,10 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
await onStart.promise

// https://specs.ipfs.tech/routing/http-routing-v1/
const resource = `${this.clientUrl}routing/v1/providers/${cid.toString()}`
const url = new URL(`${this.clientUrl}routing/v1/providers/${cid.toString()}`)
this.#addFilterParams(url, options.filterAddrs, options.filterProtocols)
const getOptions = { headers: { Accept: 'application/x-ndjson' }, signal }
const res = await fetch(resource, getOptions)
const res = await fetch(url, getOptions)

if (res.status === 404) {
// https://specs.ipfs.tech/routing/http-routing-v1/#response-status-codes
Expand Down Expand Up @@ -135,7 +140,7 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
}
}

async * getPeers (peerId: PeerId, options: AbortOptions | undefined = {}): AsyncGenerator<PeerRecord> {
async * getPeers (peerId: PeerId, options: GetPeersOptions = {}): AsyncGenerator<PeerRecord> {
log('getPeers starts: %c', peerId)

const timeoutSignal = AbortSignal.timeout(this.timeout)
Expand All @@ -153,9 +158,11 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
await onStart.promise

// https://specs.ipfs.tech/routing/http-routing-v1/
const resource = `${this.clientUrl}routing/v1/peers/${peerId.toCID().toString()}`
const url = new URL(`${this.clientUrl}routing/v1/peers/${peerId.toCID().toString()}`)
this.#addFilterParams(url, options.filterAddrs, options.filterProtocols)

const getOptions = { headers: { Accept: 'application/x-ndjson' }, signal }
const res = await fetch(resource, getOptions)
const res = await fetch(url, getOptions)

if (res.status === 404) {
// https://specs.ipfs.tech/routing/http-routing-v1/#response-status-codes
Expand Down Expand Up @@ -326,4 +333,20 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
log.error('could not conform record to peer schema', err)
}
}

#addFilterParams (url: URL, filterAddrs?: string[], filterProtocols?: string[]): void {
// IPIP-484 filtering. local options filter precedence over global filter
if (filterAddrs != null || this.filterAddrs != null) {
const adressFilter = filterAddrs?.join(',') ?? this.filterAddrs?.join(',') ?? ''
if (adressFilter !== '') {
url.searchParams.set('filter-addrs', adressFilter)
}
}
if (filterProtocols != null || this.filterProtocols != null) {
const protocolFilter = filterProtocols?.join(',') ?? this.filterProtocols?.join(',') ?? ''
if (protocolFilter !== '') {
url.searchParams.set('filter-protocols', protocolFilter)
}
}
}
}
28 changes: 25 additions & 3 deletions packages/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,26 @@ export interface PeerRecord {
Protocols: string[]
}

export interface DelegatedRoutingV1HttpApiClientInit {
export interface FilterOptions {
/**
* List of protocols to filter in the PeerRecords as defined in IPIP-484
* If undefined, PeerRecords are not filtered by protocol
*
* @see https://github.com/ipfs/specs/pull/484
* @default undefined
*/
filterProtocols?: string[]

/**
* Array of address filters to filter PeerRecords's addresses as defined in IPIP-484
* If undefined, PeerRecords are not filtered by address
*
* @see https://github.com/ipfs/specs/pull/484
* @default undefined
*/
filterAddrs?: string[]
}
export interface DelegatedRoutingV1HttpApiClientInit extends FilterOptions {
/**
* A concurrency limit to avoid request flood in web browser (default: 4)
*
Expand All @@ -88,18 +107,21 @@ export interface GetIPNSOptions extends AbortOptions {
validate?: boolean
}

export type GetProvidersOptions = FilterOptions & AbortOptions
export type GetPeersOptions = FilterOptions & AbortOptions

export interface DelegatedRoutingV1HttpApiClient {
/**
* Returns an async generator of {@link PeerRecord}s that can provide the
* content for the passed {@link CID}
*/
getProviders(cid: CID, options?: AbortOptions): AsyncGenerator<PeerRecord>
getProviders(cid: CID, options?: GetProvidersOptions): AsyncGenerator<PeerRecord>

/**
* Returns an async generator of {@link PeerRecord}s for the provided
* {@link PeerId}
*/
getPeers(peerId: PeerId, options?: AbortOptions): AsyncGenerator<PeerRecord>
getPeers(peerId: PeerId, options?: GetPeersOptions): AsyncGenerator<PeerRecord>

/**
* Returns a promise of a {@link IPNSRecord} for the given {@link MultihashDigest}
Expand Down
57 changes: 57 additions & 0 deletions packages/client/test/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,63 @@ describe('delegated-routing-v1-http-api-client', () => {
})))
})

it('should add filter parameters the query of the request url', async () => {
const providers = [{
Protocol: 'transport-bitswap',
Schema: 'bitswap',
Metadata: 'gBI=',
ID: (await generateKeyPair('Ed25519')).publicKey.toString(),
Addrs: []
}, {
Protocol: 'transport-bitswap',
Schema: 'peer',
Metadata: 'gBI=',
ID: (await generateKeyPair('Ed25519')).publicKey.toString(),
Addrs: ['/ip4/42.42.42.42/tcp/1234']
}, {
ID: (await generateKeyPair('Ed25519')).publicKey.toString(),
Addrs: []
}]

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: providers.map(prov => JSON.stringify(prov)).join('\n')
})

await all(client.getProviders(cid, { filterProtocols: ['transport-bitswap', 'unknown'], filterAddrs: ['webtransport', '!p2p-circuit'] }))

// Check if the correct URL was called with filter parameters
const lastCalledUrl = await fetch(`${process.env.ECHO_SERVER}/last-called-url`)
const lastCalledUrlText = await lastCalledUrl.text()

const searchParams = new URLSearchParams(lastCalledUrlText.split('?')[1])

expect(searchParams.get('filter-protocols')).to.equal('transport-bitswap,unknown')
expect(searchParams.get('filter-addrs')).to.equal('webtransport,!p2p-circuit')
})

it('should add filter parameters the query of the request url based on global filter', async () => {
const client = createDelegatedRoutingV1HttpApiClient(new URL(serverUrl), {
filterProtocols: ['transport-bitswap', 'unknown'],
filterAddrs: ['tcp', '!p2p-circuit']
})
const cid = CID.parse('QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn')

await all(client.getProviders(cid))

// Check if the correct URL was called with filter parameters
const lastCalledUrl = await fetch(`${process.env.ECHO_SERVER}/last-called-url`)
const lastCalledUrlText = await lastCalledUrl.text()

const searchParams = new URLSearchParams(lastCalledUrlText.split('?')[1])

expect(searchParams.get('filter-protocols')).to.equal('transport-bitswap,unknown')
expect(searchParams.get('filter-addrs')).to.equal('tcp,!p2p-circuit')
})

it('should handle non-json input', async () => {
const cid = CID.parse('QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn')

Expand Down
Loading