Skip to content

Commit 8f60822

Browse files
feat: customize ipns dnsResolvers (#445)
* feat(verified-fetch): customize ipns dnsResolvers * chore: pr comment Co-authored-by: Alex Potsides <alex@achingbrain.net> * test: fix dns-resolvers test * chore: harden custom dns resolver test * test: actually fix the custom dns resolver test The test was passing when ran in isolation, but not in the group. dns caching was causing problems so i customized the url * chore: firefox promise.any aggregate name is different * fix: createVerifiedFetch passes through custom dnsResolvers * docs: jsdoc fix from pr comment Co-authored-by: Alex Potsides <alex@achingbrain.net> * test: move custom-dns-resolvers tests * test: move content-type-parser test * docs: typo Co-authored-by: Alex Potsides <alex@achingbrain.net> * fix: dnsResolvers is passed in first arg to createVerifiedFetch --------- Co-authored-by: Alex Potsides <alex@achingbrain.net>
1 parent e92086a commit 8f60822

File tree

4 files changed

+108
-6
lines changed

4 files changed

+108
-6
lines changed

src/index.ts

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,28 @@
138138
* })
139139
* ```
140140
*
141+
* ### Custom DNS resolvers
142+
*
143+
* If you don't want to leak DNS queries to the default resolvers, you can provide your own list of DNS resolvers to `createVerifiedFetch`.
144+
*
145+
* Note that you do not need to provide both a DNS-over-HTTPS and a DNS-over-JSON resolver, and you should prefer `dnsJsonOverHttps` resolvers for usage in the browser for a smaller bundle size. See https://github.com/ipfs/helia/tree/main/packages/ipns#example---using-dns-json-over-https for more information.
146+
*
147+
* @example Customizing DNS resolvers
148+
*
149+
* ```typescript
150+
* import { createVerifiedFetch } from '@helia/verified-fetch'
151+
* import { dnsJsonOverHttps, dnsOverHttps } from '@helia/ipns/dns-resolvers'
152+
*
153+
* const fetch = await createVerifiedFetch({
154+
* gateways: ['https://trustless-gateway.link'],
155+
* routers: ['http://delegated-ipfs.dev'],
156+
* dnsResolvers: [
157+
* dnsJsonOverHttps('https://my-dns-resolver.example.com/dns-json'),
158+
* dnsOverHttps('https://my-dns-resolver.example.com/dns-query')
159+
* ]
160+
* })
161+
* ```
162+
*
141163
* ### IPLD codec handling
142164
*
143165
* IPFS supports several data formats (typically referred to as codecs) which are included in the CID. `@helia/verified-fetch` attempts to abstract away some of the details for easier consumption.
@@ -472,7 +494,7 @@ import { createHeliaHTTP } from '@helia/http'
472494
import { delegatedHTTPRouting } from '@helia/routers'
473495
import { VerifiedFetch as VerifiedFetchClass } from './verified-fetch.js'
474496
import type { Helia } from '@helia/interface'
475-
import type { IPNSRoutingEvents, ResolveDnsLinkProgressEvents, ResolveProgressEvents } from '@helia/ipns'
497+
import type { DNSResolver, IPNSRoutingEvents, ResolveDnsLinkProgressEvents, ResolveProgressEvents } from '@helia/ipns'
476498
import type { GetEvents } from '@helia/unixfs'
477499
import type { CID } from 'multiformats/cid'
478500
import type { ProgressEvent, ProgressOptions } from 'progress-events'
@@ -508,6 +530,18 @@ export interface VerifiedFetch {
508530
export interface CreateVerifiedFetchInit {
509531
gateways: string[]
510532
routers?: string[]
533+
534+
/**
535+
* In order to parse DNSLink records, we need to resolve DNS queries. You can
536+
* pass a list of DNS resolvers that we will provide to the @helia/ipns
537+
* instance for you. You must construct them using the `dnsJsonOverHttps` or
538+
* `dnsOverHttps` functions exported from `@helia/ipns/dns-resolvers`.
539+
*
540+
* We use cloudflare and google's dnsJsonOverHttps resolvers by default.
541+
*
542+
* @default [dnsJsonOverHttps('https://mozilla.cloudflare-dns.com/dns-query'),dnsJsonOverHttps('https://dns.google/resolve')]
543+
*/
544+
dnsResolvers?: DNSResolver[]
511545
}
512546

513547
export interface CreateVerifiedFetchOptions {
@@ -516,6 +550,8 @@ export interface CreateVerifiedFetchOptions {
516550
* provide will be passed the first set of bytes we receive from the network,
517551
* and should return a string that will be used as the value for the
518552
* `Content-Type` header in the response.
553+
*
554+
* @default undefined
519555
*/
520556
contentTypeParser?: ContentTypeParser
521557
}
@@ -561,9 +597,9 @@ export interface VerifiedFetchInit extends RequestInit, ProgressOptions<BubbledP
561597
* Create and return a Helia node
562598
*/
563599
export async function createVerifiedFetch (init?: Helia | CreateVerifiedFetchInit, options?: CreateVerifiedFetchOptions): Promise<VerifiedFetch> {
564-
const contentTypeParser: ContentTypeParser | undefined = options?.contentTypeParser
565-
600+
let dnsResolvers: DNSResolver[] | undefined
566601
if (!isHelia(init)) {
602+
dnsResolvers = init?.dnsResolvers
567603
init = await createHeliaHTTP({
568604
blockBrokers: [
569605
trustlessGateway({
@@ -574,7 +610,7 @@ export async function createVerifiedFetch (init?: Helia | CreateVerifiedFetchIni
574610
})
575611
}
576612

577-
const verifiedFetchInstance = new VerifiedFetchClass({ helia: init }, { contentTypeParser })
613+
const verifiedFetchInstance = new VerifiedFetchClass({ helia: init }, { dnsResolvers, ...options })
578614
async function verifiedFetch (resource: Resource, options?: VerifiedFetchInit): Promise<Response> {
579615
return verifiedFetchInstance.fetch(resource, options)
580616
}

src/verified-fetch.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { car } from '@helia/car'
2-
import { ipns as heliaIpns, type IPNS } from '@helia/ipns'
2+
import { ipns as heliaIpns, type DNSResolver, type IPNS } from '@helia/ipns'
33
import { dnsJsonOverHttps } from '@helia/ipns/dns-resolvers'
44
import { unixfs as heliaUnixFs, type UnixFS as HeliaUnixFs, type UnixFSStats } from '@helia/unixfs'
55
import * as ipldDagCbor from '@ipld/dag-cbor'
@@ -43,6 +43,7 @@ interface VerifiedFetchComponents {
4343
*/
4444
interface VerifiedFetchInit {
4545
contentTypeParser?: ContentTypeParser
46+
dnsResolvers?: DNSResolver[]
4647
}
4748

4849
interface FetchHandlerFunctionArg {
@@ -126,7 +127,7 @@ export class VerifiedFetch {
126127
this.helia = helia
127128
this.log = helia.logger.forComponent('helia:verified-fetch')
128129
this.ipns = ipns ?? heliaIpns(helia, {
129-
resolvers: [
130+
resolvers: init?.dnsResolvers ?? [
130131
dnsJsonOverHttps('https://mozilla.cloudflare-dns.com/dns-query'),
131132
dnsJsonOverHttps('https://dns.google/resolve')
132133
]

test/content-type-parser.spec.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import { stop } from '@libp2p/interface'
44
import { fileTypeFromBuffer } from '@sgtpooki/file-type'
55
import { expect } from 'aegir/chai'
66
import { filetypemime } from 'magic-bytes.js'
7+
import Sinon from 'sinon'
78
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
9+
import { createVerifiedFetch } from '../src/index.js'
810
import { VerifiedFetch } from '../src/verified-fetch.js'
911
import type { Helia } from '@helia/interface'
1012
import type { CID } from 'multiformats/cid'
@@ -26,6 +28,17 @@ describe('content-type-parser', () => {
2628
await stop(verifiedFetch)
2729
})
2830

31+
it('is used when passed to createVerifiedFetch', async () => {
32+
const contentTypeParser = Sinon.stub().resolves('text/plain')
33+
const fetch = await createVerifiedFetch(helia, {
34+
contentTypeParser
35+
})
36+
expect(fetch).to.be.ok()
37+
const resp = await fetch(cid)
38+
expect(resp.headers.get('content-type')).to.equal('text/plain')
39+
await fetch.stop()
40+
})
41+
2942
it('sets default content type if contentTypeParser is not passed', async () => {
3043
verifiedFetch = new VerifiedFetch({
3144
helia

test/custom-dns-resolvers.spec.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { stop } from '@libp2p/interface'
2+
import { expect } from 'aegir/chai'
3+
import Sinon from 'sinon'
4+
import { createVerifiedFetch } from '../src/index.js'
5+
import { VerifiedFetch } from '../src/verified-fetch.js'
6+
import { createHelia } from './fixtures/create-offline-helia.js'
7+
import type { Helia } from '@helia/interface'
8+
9+
describe('custom dns-resolvers', () => {
10+
let helia: Helia
11+
12+
beforeEach(async () => {
13+
helia = await createHelia()
14+
})
15+
16+
afterEach(async () => {
17+
await stop(helia)
18+
})
19+
20+
it('is used when passed to createVerifiedFetch', async () => {
21+
const customDnsResolver = Sinon.stub()
22+
23+
customDnsResolver.returns(Promise.resolve('/ipfs/QmVP2ip92jQuMDezVSzQBWDqWFbp9nyCHNQSiciRauPLDg'))
24+
25+
const fetch = await createVerifiedFetch({
26+
gateways: ['http://127.0.0.1:8080'],
27+
dnsResolvers: [customDnsResolver]
28+
})
29+
// error of walking the CID/dag because we haven't actually added the block to the blockstore
30+
await expect(fetch('ipns://some-non-cached-domain.com')).to.eventually.be.rejected.with.property('errors')
31+
32+
expect(customDnsResolver.callCount).to.equal(1)
33+
expect(customDnsResolver.getCall(0).args).to.deep.equal(['some-non-cached-domain.com', { onProgress: undefined }])
34+
})
35+
36+
it('is used when passed to VerifiedFetch', async () => {
37+
const customDnsResolver = Sinon.stub()
38+
39+
customDnsResolver.returns(Promise.resolve('/ipfs/QmVP2ip92jQuMDezVSzQBWDqWFbp9nyCHNQSiciRauPLDg'))
40+
41+
const verifiedFetch = new VerifiedFetch({
42+
helia
43+
}, {
44+
dnsResolvers: [customDnsResolver]
45+
})
46+
// error of walking the CID/dag because we haven't actually added the block to the blockstore
47+
await expect(verifiedFetch.fetch('ipns://some-non-cached-domain2.com')).to.eventually.be.rejected.with.property('errors').that.has.lengthOf(0)
48+
49+
expect(customDnsResolver.callCount).to.equal(1)
50+
expect(customDnsResolver.getCall(0).args).to.deep.equal(['some-non-cached-domain2.com', { onProgress: undefined }])
51+
})
52+
})

0 commit comments

Comments
 (0)