From c85ca667edeb7f7e3f033696cce6f8bcb04ac4d7 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Tue, 5 Nov 2024 12:15:33 -0600 Subject: [PATCH] feat: return IPNSRecords for ipns subdomain URLs --- packages/verified-fetch/src/verified-fetch.ts | 29 +++++++++++--- .../verified-fetch/test/accept-header.spec.ts | 39 +++++++++++++++++++ 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/packages/verified-fetch/src/verified-fetch.ts b/packages/verified-fetch/src/verified-fetch.ts index 0751601..7476edc 100644 --- a/packages/verified-fetch/src/verified-fetch.ts +++ b/packages/verified-fetch/src/verified-fetch.ts @@ -5,11 +5,12 @@ import * as ipldDagJson from '@ipld/dag-json' import { code as dagPbCode } from '@ipld/dag-pb' import { type AbortOptions, type Logger, type PeerId } from '@libp2p/interface' import { Record as DHTRecord } from '@libp2p/kad-dht' -import { peerIdFromString } from '@libp2p/peer-id' +import { peerIdFromCID, peerIdFromString } from '@libp2p/peer-id' import { Key } from 'interface-datastore' import { exporter } from 'ipfs-unixfs-exporter' import toBrowserReadableStream from 'it-to-browser-readablestream' import { LRUCache } from 'lru-cache' +import { CID } from 'multiformats/cid' import { code as jsonCode } from 'multiformats/codecs/json' import { code as rawCode } from 'multiformats/codecs/raw' import { identity } from 'multiformats/hashes/identity' @@ -37,7 +38,6 @@ import type { FetchHandlerFunctionArg, RequestFormatShorthand } from './types.js import type { Helia, SessionBlockstore } from '@helia/interface' import type { Blockstore } from 'interface-blockstore' import type { ObjectNode } from 'ipfs-unixfs-exporter' -import type { CID } from 'multiformats/cid' const SESSION_CACHE_MAX_SIZE = 100 const SESSION_CACHE_TTL_MS = 60 * 1000 @@ -144,20 +144,37 @@ export class VerifiedFetch { } /** - * Accepts an `ipns://...` URL as a string and returns a `Response` containing + * Accepts an `ipns://...` or `https?://.ipns.` URL as a string and returns a `Response` containing * a raw IPNS record. */ private async handleIPNSRecord ({ resource, cid, path, options }: FetchHandlerFunctionArg): Promise { - if (path !== '' || !resource.startsWith('ipns://')) { + if (path !== '' || !(resource.startsWith('ipns://') || resource.includes('.ipns.'))) { + this.log.error('invalid request for IPNS name "%s" and path "%s"', resource, path) return badRequestResponse(resource, 'Invalid IPNS name') } let peerId: PeerId try { - peerId = peerIdFromString(resource.replace('ipns://', '')) + if (resource.startsWith('ipns://')) { + const peerIdString = resource.replace('ipns://', '') + this.log.trace('trying to parse peer id from "%s"', peerIdString) + peerId = peerIdFromString(peerIdString) + } else { + const peerIdString = resource.split('.ipns.')[0].split('://')[1] + this.log.trace('trying to parse peer id from "%s"', peerIdString) + let cid: CID + try { + cid = CID.parse(peerIdString) + } catch (err: any) { + this.log.error('could not construct CID from peerId string "%s"', resource, err) + return badRequestResponse(resource, err) + } + + peerId = peerIdFromCID(cid) + } } catch (err: any) { - this.log.error('could not parse peer id from IPNS url %s', resource) + this.log.error('could not parse peer id from IPNS url %s', resource, err) return badRequestResponse(resource, err) } diff --git a/packages/verified-fetch/test/accept-header.spec.ts b/packages/verified-fetch/test/accept-header.spec.ts index 0205b33..368920d 100644 --- a/packages/verified-fetch/test/accept-header.spec.ts +++ b/packages/verified-fetch/test/accept-header.spec.ts @@ -9,6 +9,7 @@ import { peerIdFromPrivateKey } from '@libp2p/peer-id' import { expect } from 'aegir/chai' import * as cborg from 'cborg' import { marshalIPNSRecord } from 'ipns' +import { base36 } from 'multiformats/bases/base36' import { CID } from 'multiformats/cid' import * as raw from 'multiformats/codecs/raw' import { sha256 } from 'multiformats/hashes/sha2' @@ -294,6 +295,44 @@ describe('accept header', () => { expect(new Uint8Array(buf)).to.equalBytes(marshalIPNSRecord(record)) }) + it('should support fetching IPNS records for a ipns subdomain', async () => { + const key = await generateKeyPair('Ed25519') + const peerId = peerIdFromPrivateKey(key) + + const obj = { + hello: 'world' + } + const c = dagCbor(helia) + const cid = await c.add(obj) + + const i = ipns(helia) + const record = await i.publish(key, cid) + + /** + * Works with k51... peerIds + */ + let resp = await verifiedFetch.fetch(`http://${peerId.toCID().toString(base36)}.ipns.example.com`, { + headers: { + accept: 'application/vnd.ipfs.ipns-record' + } + }) + expect(resp.status).to.equal(200) + expect(resp.headers.get('content-type')).to.equal('application/vnd.ipfs.ipns-record') + expect(new Uint8Array(await resp.arrayBuffer())).to.equalBytes(marshalIPNSRecord(record)) + + /** + * Works with default CID peerIds + */ + resp = await verifiedFetch.fetch(`http://${peerId.toCID().toString()}.ipns.example.com`, { + headers: { + accept: 'application/vnd.ipfs.ipns-record' + } + }) + expect(resp.status).to.equal(200) + expect(resp.headers.get('content-type')).to.equal('application/vnd.ipfs.ipns-record') + expect(new Uint8Array(await resp.arrayBuffer())).to.equalBytes(marshalIPNSRecord(record)) + }) + shouldNotAcceptCborWith({ obj: { hello: 'world',