Skip to content

Commit 2ac0e8b

Browse files
authored
feat!: support DNS over HTTPS and DNS-JSON over HTTPS (#55)
Co-authored-by: Russell Dempsey <[email protected]> BREAKING CHANGE: alters the options object passed to the `ipns` factory function
1 parent d954e0a commit 2ac0e8b

22 files changed

+729
-176
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,6 @@ node_modules
77
package-lock.json
88
yarn.lock
99
.vscode
10+
.env
11+
.envrc
12+
.tool-versions

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"docs:no-publish": "NODE_OPTIONS=--max_old_space_size=8192 aegir docs --publish false -- --exclude packages/interop"
3737
},
3838
"devDependencies": {
39-
"aegir": "^41.0.0",
39+
"aegir": "^41.1.14",
4040
"npm-run-all": "^4.1.5"
4141
},
4242
"type": "module",

packages/interop/.aegir.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export default {
1212
host: '127.0.0.1',
1313
port: ipfsdPort
1414
}, {
15-
ipfsBin: (await import('go-ipfs')).default.path(),
15+
ipfsBin: (await import('kubo')).default.path(),
1616
kuboRpcModule: kuboRpcClient,
1717
ipfsOptions: {
1818
config: {

packages/interop/package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,16 +60,15 @@
6060
"@libp2p/peer-id-factory": "^3.0.3",
6161
"@libp2p/tcp": "^8.0.4",
6262
"@libp2p/websockets": "^7.0.4",
63-
"aegir": "^41.0.0",
6463
"blockstore-core": "^4.0.1",
6564
"datastore-core": "^9.0.3",
66-
"go-ipfs": "^0.22.0",
6765
"helia": "^2.0.1",
6866
"ipfsd-ctl": "^13.0.0",
6967
"ipns": "^7.0.1",
7068
"it-all": "^3.0.2",
7169
"it-last": "^3.0.1",
7270
"it-map": "^3.0.3",
71+
"kubo": "^0.24.0",
7372
"kubo-rpc-client": "^3.0.0",
7473
"libp2p": "^0.46.6",
7574
"merge-options": "^3.0.4",
@@ -79,7 +78,7 @@
7978
},
8079
"browser": {
8180
"./dist/test/fixtures/create-helia.js": "./dist/test/fixtures/create-helia.browser.js",
82-
"go-ipfs": false
81+
"kubo": false
8382
},
8483
"private": true
8584
}

packages/interop/test/dht.spec.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,11 @@ keyTypes.forEach(type => {
131131
message: 'Kubo could not find Helia on the DHT'
132132
})
133133

134-
name = ipns(helia, [
135-
dht(helia)
136-
])
134+
name = ipns(helia, {
135+
routers: [
136+
dht(helia)
137+
]
138+
})
137139
}
138140

139141
afterEach(async () => {

packages/interop/test/fixtures/create-kubo.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
1-
/* eslint-disable @typescript-eslint/ban-ts-comment,@typescript-eslint/prefer-ts-expect-error */
2-
// @ts-ignore no types - TODO: remove me once the next version of npm-go-ipfs has shipped
3-
import * as goIpfs from 'go-ipfs'
41
import { type Controller, type ControllerOptions, createController } from 'ipfsd-ctl'
2+
import * as kubo from 'kubo'
53
import * as kuboRpcClient from 'kubo-rpc-client'
64
import mergeOptions from 'merge-options'
75
import { isElectronMain, isNode } from 'wherearewe'
86

97
export async function createKuboNode (options: ControllerOptions<'go'> = {}): Promise<Controller> {
108
const opts = mergeOptions({
119
kuboRpcModule: kuboRpcClient,
12-
ipfsBin: isNode || isElectronMain ? goIpfs.path() : undefined,
10+
ipfsBin: isNode || isElectronMain ? kubo.path() : undefined,
1311
test: true,
1412
endpoint: process.env.IPFSD_SERVER,
1513
ipfsOptions: {

packages/interop/test/pubsub.spec.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,11 @@ keyTypes.filter(keyType => keyType !== 'RSA').forEach(keyType => {
5252
// connect the two nodes
5353
await connect(helia, kubo, '/meshsub/1.1.0')
5454

55-
name = ipns(helia, [
56-
pubsub(helia)
57-
])
55+
name = ipns(helia, {
56+
routers: [
57+
pubsub(helia)
58+
]
59+
})
5860
})
5961

6062
afterEach(async () => {

packages/ipns/package.json

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@
4646
"./routing": {
4747
"types": "./dist/src/routing/index.d.ts",
4848
"import": "./dist/src/routing/index.js"
49+
},
50+
"./dns-resolvers": {
51+
"types": "./dist/src/dns-resolvers/index.d.ts",
52+
"import": "./dist/src/dns-resolvers/index.js"
4953
}
5054
},
5155
"eslintConfig": {
@@ -155,10 +159,11 @@
155159
"release": "aegir release"
156160
},
157161
"dependencies": {
158-
"@libp2p/interface": "^0.1.2",
159162
"@libp2p/kad-dht": "^10.0.11",
160163
"@libp2p/logger": "^3.0.2",
161164
"@libp2p/peer-id": "^3.0.2",
165+
"dns-over-http-resolver": "^2.1.3",
166+
"dns-packet": "^5.6.0",
162167
"hashlru": "^2.3.0",
163168
"interface-datastore": "^8.0.0",
164169
"ipns": "^7.0.1",
@@ -169,13 +174,17 @@
169174
"uint8arrays": "^4.0.3"
170175
},
171176
"devDependencies": {
177+
"@libp2p/interface": "^0.1.4",
172178
"@libp2p/peer-id-factory": "^3.0.3",
173-
"aegir": "^41.0.0",
179+
"@types/dns-packet": "^5.6.4",
174180
"datastore-core": "^9.0.3",
175181
"sinon": "^17.0.0",
176182
"sinon-ts": "^1.0.0"
177183
},
178184
"browser": {
179-
"./dist/src/utils/resolve-dns-link.js": "./dist/src/utils/resolve-dns-link.browser.js"
185+
"./dist/src/dns-resolvers/resolver.js": "./dist/src/dns-resolvers/resolver.browser.js"
186+
},
187+
"typedoc": {
188+
"entryPoint": "./src/index.ts"
180189
}
181190
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { MAX_RECURSIVE_DEPTH, recursiveResolveDnslink } from '../utils/dns.js'
2+
import resolve from './resolver.js'
3+
import type { DNSResolver, ResolveDnsLinkOptions } from '../index.js'
4+
5+
export function defaultResolver (): DNSResolver {
6+
return async (domain: string, options: ResolveDnsLinkOptions = {}): Promise<string> => {
7+
return recursiveResolveDnslink(domain, MAX_RECURSIVE_DEPTH, resolve, options)
8+
}
9+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/* eslint-env browser */
2+
3+
import PQueue from 'p-queue'
4+
import { CustomProgressEvent } from 'progress-events'
5+
import { type DNSResponse, MAX_RECURSIVE_DEPTH, recursiveResolveDnslink, ipfsPathAndAnswer } from '../utils/dns.js'
6+
import { TLRU } from '../utils/tlru.js'
7+
import type { ResolveDnsLinkOptions, DNSResolver } from '../index.js'
8+
9+
// Avoid sending multiple queries for the same hostname by caching results
10+
const cache = new TLRU<string>(1000)
11+
// This TTL will be used if the remote service does not return one
12+
const ttl = 60 * 1000
13+
14+
/**
15+
* Uses the RFC 8427 'application/dns-json' content-type to resolve DNS queries.
16+
*
17+
* Supports and server that uses the same schema as Google's DNS over HTTPS
18+
* resolver.
19+
*
20+
* This resolver needs fewer dependencies than the regular DNS-over-HTTPS
21+
* resolver so can result in a smaller bundle size and consequently is preferred
22+
* for browser use.
23+
*
24+
* @see https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/make-api-requests/dns-json/
25+
* @see https://github.com/curl/curl/wiki/DNS-over-HTTPS#publicly-available-servers
26+
* @see https://dnsprivacy.org/public_resolvers/
27+
* @see https://datatracker.ietf.org/doc/html/rfc8427
28+
*/
29+
export function dnsJsonOverHttps (url: string): DNSResolver {
30+
// browsers limit concurrent connections per host,
31+
// we don't want preload calls to exhaust the limit (~6)
32+
const httpQueue = new PQueue({ concurrency: 4 })
33+
34+
const resolve = async (fqdn: string, options: ResolveDnsLinkOptions = {}): Promise<string> => {
35+
const searchParams = new URLSearchParams()
36+
searchParams.set('name', fqdn)
37+
searchParams.set('type', 'TXT')
38+
39+
const query = searchParams.toString()
40+
41+
// try cache first
42+
if (options.nocache !== true && cache.has(query)) {
43+
const response = cache.get(query)
44+
45+
if (response != null) {
46+
options.onProgress?.(new CustomProgressEvent<string>('dnslink:cache', { detail: response }))
47+
return response
48+
}
49+
}
50+
51+
options.onProgress?.(new CustomProgressEvent<string>('dnslink:query', { detail: fqdn }))
52+
53+
// query DNS-JSON over HTTPS server
54+
const response = await httpQueue.add(async () => {
55+
const res = await fetch(`${url}?${searchParams}`, {
56+
headers: {
57+
accept: 'application/dns-json'
58+
},
59+
signal: options.signal
60+
})
61+
62+
if (res.status !== 200) {
63+
throw new Error(`Unexpected HTTP status: ${res.status} - ${res.statusText}`)
64+
}
65+
66+
const query = new URL(res.url).search.slice(1)
67+
const json: DNSResponse = await res.json()
68+
69+
options.onProgress?.(new CustomProgressEvent<DNSResponse>('dnslink:answer', { detail: json }))
70+
71+
const { ipfsPath, answer } = ipfsPathAndAnswer(fqdn, json)
72+
73+
cache.set(query, ipfsPath, answer.TTL ?? ttl)
74+
75+
return ipfsPath
76+
}, {
77+
signal: options.signal
78+
})
79+
80+
if (response == null) {
81+
throw new Error('No DNS response received')
82+
}
83+
84+
return response
85+
}
86+
87+
return async (domain: string, options: ResolveDnsLinkOptions = {}) => {
88+
return recursiveResolveDnslink(domain, MAX_RECURSIVE_DEPTH, resolve, options)
89+
}
90+
}

0 commit comments

Comments
 (0)