Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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[] | undefined
private readonly filterProtocols: string[] | undefined

/**
* 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