Skip to content

Commit

Permalink
feat: return IPNSRecords for ipns subdomain URLs (#131)
Browse files Browse the repository at this point in the history
  • Loading branch information
SgtPooki authored Nov 13, 2024
1 parent 4e5669f commit 05b7ac6
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 6 deletions.
29 changes: 23 additions & 6 deletions packages/verified-fetch/src/verified-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -144,20 +144,37 @@ export class VerifiedFetch {
}

/**
* Accepts an `ipns://...` URL as a string and returns a `Response` containing
* Accepts an `ipns://...` or `https?://<ipnsname>.ipns.<domain>` URL as a string and returns a `Response` containing
* a raw IPNS record.
*/
private async handleIPNSRecord ({ resource, cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
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)
}
Expand Down
39 changes: 39 additions & 0 deletions packages/verified-fetch/test/accept-header.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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',
Expand Down

0 comments on commit 05b7ac6

Please sign in to comment.