@multiformats/dns
Version:
Resolve DNS queries with browser fallback
111 lines (90 loc) • 3.21 kB
text/typescript
/* eslint-env browser */
import { Buffer } from 'buffer'
import dnsPacket from 'dns-packet'
import PQueue from 'p-queue'
import { CustomProgressEvent } from 'progress-events'
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
import { RecordType } from '../index.js'
import { getTypes } from '../utils/get-types.js'
import { toDNSResponse } from '../utils/to-dns-response.js'
import type { DNSResolver } from './index.js'
import type { DNSResponse } from '../index.js'
/**
* Browsers limit concurrent connections per host (~6), we don't want to exhaust
* the limit so this value controls how many DNS queries can be in flight at
* once.
*/
export const DEFAULT_QUERY_CONCURRENCY = 4
export interface DNSOverHTTPSOptions {
queryConcurrency?: number
}
function toType (type: RecordType): 'A' | 'AAAA' | 'TXT' | 'CNAME' {
if (type === RecordType.A) {
return 'A'
}
if (type === RecordType.AAAA) {
return 'AAAA'
}
if (type === RecordType.TXT) {
return 'TXT'
}
if (type === RecordType.CNAME) {
return 'CNAME'
}
throw new Error('Unsupported DNS record type')
}
/**
* Uses the RFC 1035 'application/dns-message' content-type to resolve DNS
* queries.
*
* This resolver needs more dependencies than the non-standard
* DNS-JSON-over-HTTPS resolver so can result in a larger bundle size and
* consequently is not preferred for browser use.
*
* @see https://datatracker.ietf.org/doc/html/rfc1035
* @see https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/make-api-requests/dns-wireformat/
* @see https://github.com/curl/curl/wiki/DNS-over-HTTPS#publicly-available-servers
* @see https://dnsprivacy.org/public_resolvers/
*/
export function dnsOverHttps (url: string, init: DNSOverHTTPSOptions = {}): DNSResolver {
const httpQueue = new PQueue({
concurrency: init.queryConcurrency ?? DEFAULT_QUERY_CONCURRENCY
})
return async (fqdn, options = {}) => {
const types = getTypes(options.types)
const dnsQuery = dnsPacket.encode({
type: 'query',
id: 0,
flags: dnsPacket.RECURSION_DESIRED,
questions: types.map(type => ({
type: toType(type),
name: fqdn
}))
})
const searchParams = new URLSearchParams()
searchParams.set('dns', uint8ArrayToString(dnsQuery, 'base64url'))
options.onProgress?.(new CustomProgressEvent<string>('dns:query', { detail: fqdn }))
// query DNS over HTTPS server
const response = await httpQueue.add(async () => {
const res = await fetch(`${url}?${searchParams}`, {
headers: {
accept: 'application/dns-message'
},
signal: options.signal
})
if (res.status !== 200) {
throw new Error(`Unexpected HTTP status: ${res.status} - ${res.statusText}`)
}
const buf = await res.arrayBuffer()
const response = toDNSResponse(dnsPacket.decode(Buffer.from(buf)))
options.onProgress?.(new CustomProgressEvent<DNSResponse>('dns:response', { detail: response }))
return response
}, {
signal: options.signal
})
if (response == null) {
throw new Error('No DNS response received')
}
return response
}
}