diff --git a/packages/client/.aegir.js b/packages/client/.aegir.js index 02de245..14e7890 100644 --- a/packages/client/.aegir.js +++ b/packages/client/.aegir.js @@ -6,6 +6,7 @@ const options = { test: { before: async () => { let callCount = 0 + let lastCalledUrl = '' const providers = new Map() const peers = new Map() const ipnsGet = new Map() @@ -13,6 +14,10 @@ const options = { 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) @@ -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) => { @@ -68,6 +72,9 @@ const options = { callCount = 0 res.end() }) + echo.polka.get('/last-called-url', (req, res) => { + res.end(lastCalledUrl) + }) await echo.start() diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index fb01200..9173b2a 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -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' @@ -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 @@ -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) } @@ -70,7 +74,7 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV this.started = false } - async * getProviders (cid: CID, options: AbortOptions = {}): AsyncGenerator { + async * getProviders (cid: CID, options: GetProvidersOptions = {}): AsyncGenerator { log('getProviders starts: %c', cid) const timeoutSignal = AbortSignal.timeout(this.timeout) @@ -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 @@ -135,7 +140,7 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV } } - async * getPeers (peerId: PeerId, options: AbortOptions | undefined = {}): AsyncGenerator { + async * getPeers (peerId: PeerId, options: GetPeersOptions = {}): AsyncGenerator { log('getPeers starts: %c', peerId) const timeoutSignal = AbortSignal.timeout(this.timeout) @@ -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 @@ -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) + } + } + } } diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 7dd4bcf..101d88e 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -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) * @@ -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 + getProviders(cid: CID, options?: GetProvidersOptions): AsyncGenerator /** * Returns an async generator of {@link PeerRecord}s for the provided * {@link PeerId} */ - getPeers(peerId: PeerId, options?: AbortOptions): AsyncGenerator + getPeers(peerId: PeerId, options?: GetPeersOptions): AsyncGenerator /** * Returns a promise of a {@link IPNSRecord} for the given {@link MultihashDigest} diff --git a/packages/client/test/index.spec.ts b/packages/client/test/index.spec.ts index aa0be1e..70e5d90 100644 --- a/packages/client/test/index.spec.ts +++ b/packages/client/test/index.spec.ts @@ -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')