diff --git a/packages/client/.aegir.js b/packages/client/.aegir.js index 411adee..739d8ce 100644 --- a/packages/client/.aegir.js +++ b/packages/client/.aegir.js @@ -6,17 +6,51 @@ const options = { test: { before: async () => { 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.post('/add-providers/:cid', (req, res) => { providers.set(req.params.cid, req.body) res.end() }) echo.polka.get('/routing/v1/providers/:cid', (req, res) => { - const provs = providers.get(req.params.cid) ?? '[]' + const records = providers.get(req.params.cid) ?? '[]' providers.delete(req.params.cid) - res.end(provs) + res.end(records) + }) + echo.polka.post('/add-peers/:peerId', (req, res) => { + peers.set(req.params.peerId, req.body) + res.end() + }) + echo.polka.get('/routing/v1/peers/:peerId', (req, res) => { + const records = peers.get(req.params.peerId) ?? '[]' + peers.delete(req.params.peerId) + + res.end(records) + }) + echo.polka.post('/add-ipns/:peerId', (req, res) => { + ipnsGet.set(req.params.peerId, req.body) + res.end() + }) + echo.polka.get('/routing/v1/ipns/:peerId', (req, res) => { + const record = ipnsGet.get(req.params.peerId) ?? '' + ipnsGet.delete(req.params.peerId) + + res.end(record) + }) + echo.polka.put('/routing/v1/ipns/:peerId', (req, res) => { + ipnsPut.set(req.params.peerId, req.body) + res.end() + }) + echo.polka.get('/get-ipns/:peerId', (req, res) => { + const record = ipnsPut.get(req.params.peerId) ?? '' + ipnsPut.delete(req.params.peerId) + + res.end(record) }) await echo.start() diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index 91aa7c8..64f4408 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -4,8 +4,8 @@ import { peerIdFromString } from '@libp2p/peer-id' import { multiaddr } from '@multiformats/multiaddr' import { anySignal } from 'any-signal' import toIt from 'browser-readablestream-to-it' -import { unmarshal, type IPNSRecord, marshal } from 'ipns' -import toBuffer from 'it-to-buffer' +import { unmarshal, type IPNSRecord, marshal, peerIdToRoutingKey } from 'ipns' +import { ipnsValidator } from 'ipns/validator' // @ts-expect-error no types import ndjson from 'iterable-ndjson' import defer from 'p-defer' @@ -74,15 +74,15 @@ export class DefaultRoutingV1HttpApiClient implements RoutingV1HttpApiClient { // https://specs.ipfs.tech/routing/http-routing-v1/ const resource = `${this.clientUrl}routing/v1/providers/${cid.toString()}` const getOptions = { headers: { Accept: 'application/x-ndjson' }, signal } - const a = await fetch(resource, getOptions) + const res = await fetch(resource, getOptions) - if (a.body == null) { + if (res.body == null) { throw new CodeError('Routing response had no body', 'ERR_BAD_RESPONSE') } - const contentType = a.headers.get('Content-Type') + const contentType = res.headers.get('Content-Type') if (contentType === 'application/json') { - const body = await a.json() + const body = await res.json() for (const provider of body.Providers) { const record = this.#handleProviderRecords(provider) @@ -91,7 +91,7 @@ export class DefaultRoutingV1HttpApiClient implements RoutingV1HttpApiClient { } } } else { - for await (const provider of ndjson(toIt(a.body))) { + for await (const provider of ndjson(toIt(res.body))) { const record = this.#handleProviderRecords(provider) if (record !== null) { yield record @@ -125,25 +125,25 @@ export class DefaultRoutingV1HttpApiClient implements RoutingV1HttpApiClient { // https://specs.ipfs.tech/routing/http-routing-v1/ const resource = `${this.clientUrl}routing/v1/peers/${peerId.toCID().toString()}` const getOptions = { headers: { Accept: 'application/x-ndjson' }, signal } - const a = await fetch(resource, getOptions) + const res = await fetch(resource, getOptions) - if (a.body == null) { + if (res.body == null) { throw new CodeError('Routing response had no body', 'ERR_BAD_RESPONSE') } - const contentType = a.headers.get('Content-Type') + const contentType = res.headers.get('Content-Type') if (contentType === 'application/json') { - const body = await a.json() + const body = await res.json() for (const peer of body.Peers) { - const record = this.#handlePeerRecords(peer) + const record = this.#handlePeerRecords(peerId, peer) if (record !== null) { yield record } } } else { - for await (const peer of ndjson(toIt(a.body))) { - const record = this.#handlePeerRecords(peer) + for await (const peer of ndjson(toIt(res.body))) { + const record = this.#handlePeerRecords(peerId, peer) if (record !== null) { yield record } @@ -176,13 +176,14 @@ export class DefaultRoutingV1HttpApiClient implements RoutingV1HttpApiClient { // https://specs.ipfs.tech/routing/http-routing-v1/ const resource = `${this.clientUrl}routing/v1/ipns/${peerId.toCID().toString()}` const getOptions = { headers: { Accept: 'application/vnd.ipfs.ipns-record' }, signal } - const a = await fetch(resource, getOptions) + const res = await fetch(resource, getOptions) - if (a.body == null) { + if (res.body == null) { throw new CodeError('GET ipns response had no body', 'ERR_BAD_RESPONSE') } - const body = await toBuffer(toIt(a.body)) + const body = new Uint8Array(await res.arrayBuffer()) + await ipnsValidator(peerIdToRoutingKey(peerId), body) return unmarshal(body) } finally { signal.clear() @@ -241,12 +242,14 @@ export class DefaultRoutingV1HttpApiClient implements RoutingV1HttpApiClient { return null } - #handlePeerRecords (record: any): PeerRecord | null { + #handlePeerRecords (peerId: PeerId, record: any): PeerRecord | null { if (record.Schema === 'peer') { // Peer schema can have additional, user-defined, fields. record.ID = peerIdFromString(record.ID) record.Addrs = record.Addrs.map(multiaddr) - return record + if (peerId.equals(record.ID)) { + return record + } } return null diff --git a/packages/client/test/index.spec.ts b/packages/client/test/index.spec.ts index 8f36cbc..211edff 100644 --- a/packages/client/test/index.spec.ts +++ b/packages/client/test/index.spec.ts @@ -2,6 +2,7 @@ import { createEd25519PeerId } from '@libp2p/peer-id-factory' import { expect } from 'aegir/chai' +import { create as createIpnsRecord, marshal as marshalIpnsRecord } from 'ipns' import all from 'it-all' import { CID } from 'multiformats/cid' import { createRoutingV1HttpApiClient, type RoutingV1HttpApiClient } from '../src/index.js' @@ -103,4 +104,103 @@ describe('routing-v1-http-api-client', () => { const provs = await all(client.getProviders(cid)) expect(provs).to.be.empty() }) + + it('should find peers and only accepts correct peer records', async () => { + const peerId = await createEd25519PeerId() + + const records = [{ + Protocol: 'transport-bitswap', + Schema: 'bitswap', + Metadata: 'gBI=', + ID: peerId.toString(), + Addrs: ['/ip4/41.41.41.41/tcp/1234'] + }, { + Protocol: 'transport-saddle', + Schema: 'horse-ride', + Metadata: 'gBI=', + ID: peerId.toString(), + Addrs: ['/ip4/41.41.41.41/tcp/1234'] + }, { + Protocols: ['transport-bitswap'], + Schema: 'peer', + Metadata: 'gBI=', + ID: peerId.toString(), + Addrs: ['/ip4/42.42.42.42/tcp/1234'] + }, { + Protocols: ['transport-bitswap'], + Schema: 'peer', + Metadata: 'gBI=', + ID: (await createEd25519PeerId()).toString(), + Addrs: ['/ip4/42.42.42.42/tcp/1234'] + }] + + // load peer for the router to fetch + await fetch(`${process.env.ECHO_SERVER}/add-peers/${peerId.toCID().toString()}`, { + method: 'POST', + body: records.map(prov => JSON.stringify(prov)).join('\n') + }) + + const peerRecords = await all(client.getPeerInfo(peerId)) + expect(peerRecords.map(peerRecord => ({ + ...peerRecord, + ID: peerRecord.ID.toString(), + Addrs: peerRecord.Addrs.map(ma => ma.toString()) + }))).to.deep.equal([ + records[2] + ]) + }) + + it('should get ipns record', async () => { + const cid = CID.parse('QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn') + const peerId = await createEd25519PeerId() + const record = await createIpnsRecord(peerId, cid, 0, 1000) + + // load record for the router to fetch + await fetch(`${process.env.ECHO_SERVER}/add-ipns/${peerId.toCID().toString()}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/vnd.ipfs.ipns-record' + }, + body: marshalIpnsRecord(record) + }) + + const ipnsRecord = await client.getIPNS(peerId) + expect(marshalIpnsRecord(ipnsRecord)).to.equalBytes(marshalIpnsRecord(record)) + }) + + it('get ipns record fails with bad record', async () => { + const cid = CID.parse('QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn') + const peerId = await createEd25519PeerId() + const record = await createIpnsRecord(await createEd25519PeerId(), cid, 0, 1000) + + // load record for the router to fetch + await fetch(`${process.env.ECHO_SERVER}/add-ipns/${peerId.toCID().toString()}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/vnd.ipfs.ipns-record' + }, + body: marshalIpnsRecord(record) + }) + + await expect(client.getIPNS(peerId)).to.be.rejected() + }) + + it('should put ipns', async () => { + const cid = CID.parse('QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn') + const peerId = await createEd25519PeerId() + const record = await createIpnsRecord(peerId, cid, 0, 1000) + + await client.putIPNS(peerId, record) + + // load record that our client just PUT to remote server + const res = await fetch(`${process.env.ECHO_SERVER}/get-ipns/${peerId.toCID().toString()}`, { + method: 'GET', + headers: { + Accept: 'application/vnd.ipfs.ipns-record' + } + }) + + const receivedRecord = new Uint8Array(await res.arrayBuffer()) + expect(marshalIpnsRecord(record)).to.equalBytes(receivedRecord) + }) })